/*
* Copyright (C) 2010-2013 Paul Watts (paulcwatts@gmail.com)
* and individual contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onebusaway.android.util;
import com.google.android.gms.common.GoogleApiAvailability;
import org.onebusaway.android.R;
import org.onebusaway.android.app.Application;
import org.onebusaway.android.io.ObaApi;
import org.onebusaway.android.io.elements.ObaArrivalInfo;
import org.onebusaway.android.io.elements.ObaRegion;
import org.onebusaway.android.io.elements.ObaRoute;
import org.onebusaway.android.io.elements.ObaSituation;
import org.onebusaway.android.io.elements.ObaStop;
import org.onebusaway.android.io.request.ObaArrivalInfoResponse;
import org.onebusaway.android.map.MapParams;
import org.onebusaway.android.provider.ObaContract;
import org.onebusaway.android.ui.HomeActivity;
import org.onebusaway.android.view.RealtimeIndicatorView;
import org.onebusaway.util.comparators.AlphanumComparator;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.content.ContentQueryMap;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.location.Location;
import android.media.ExifInterface;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Parcelable;
import android.os.SystemClock;
import android.provider.Settings;
import android.support.v4.app.Fragment;
import android.support.v4.util.Pair;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.SearchView;
import android.text.Spannable;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.Log;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* A class containing utility methods related to the user interface
*/
public final class UIUtils {
private static final String TAG = "UIHelp";
public static void setupActionBar(AppCompatActivity activity) {
ActionBar bar = activity.getSupportActionBar();
bar.setIcon(android.R.color.transparent);
bar.setDisplayShowTitleEnabled(true);
// HomeActivity is the root for all other activities
if (!(activity instanceof HomeActivity)) {
bar.setDisplayHomeAsUpEnabled(true);
}
}
/**
* Sets up the search view in the action bar
*/
public static void setupSearch(Activity activity, Menu menu) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
SearchManager searchManager =
(SearchManager) activity.getSystemService(Context.SEARCH_SERVICE);
final MenuItem searchMenu = menu.findItem(R.id.action_search);
SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchMenu);
searchView.setSearchableInfo(
searchManager.getSearchableInfo(activity.getComponentName()));
// Close the keyboard and SearchView at same time when the back button is pressed
searchView.setOnQueryTextFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean queryTextFocused) {
if (!queryTextFocused) {
MenuItemCompat.collapseActionView(searchMenu);
}
}
});
}
}
public static void showProgress(Fragment fragment, boolean visible) {
AppCompatActivity act = (AppCompatActivity) fragment.getActivity();
if (act != null) {
act.setSupportProgressBarIndeterminateVisibility(visible);
}
}
public static void setClickableSpan(TextView v, ClickableSpan span) {
Spannable text = (Spannable) v.getText();
text.setSpan(span, 0, text.length(), 0);
v.setMovementMethod(LinkMovementMethod.getInstance());
}
public static void removeAllClickableSpans(TextView v) {
Spannable text = (Spannable) v.getText();
ClickableSpan[] spans = text.getSpans(0, text.length(), ClickableSpan.class);
for (ClickableSpan cs : spans) {
text.removeSpan(cs);
}
}
public static int getStopDirectionText(String direction) {
if (direction.equals("N")) {
return R.string.direction_n;
} else if (direction.equals("NW")) {
return R.string.direction_nw;
} else if (direction.equals("W")) {
return R.string.direction_w;
} else if (direction.equals("SW")) {
return R.string.direction_sw;
} else if (direction.equals("S")) {
return R.string.direction_s;
} else if (direction.equals("SE")) {
return R.string.direction_se;
} else if (direction.equals("E")) {
return R.string.direction_e;
} else if (direction.equals("NE")) {
return R.string.direction_ne;
} else {
return R.string.direction_none;
}
}
public static String getRouteDisplayName(String routeShortName, String routeLongName) {
if (!TextUtils.isEmpty(routeShortName)) {
return routeShortName;
}
if (!TextUtils.isEmpty(routeLongName)) {
return routeLongName;
}
// Just so we never return null.
return "";
}
public static String getRouteDisplayName(ObaRoute route) {
return getRouteDisplayName(route.getShortName(), route.getLongName());
}
public static String getRouteDisplayName(ObaArrivalInfo arrivalInfo) {
return getRouteDisplayName(arrivalInfo.getShortName(), arrivalInfo.getRouteLongName());
}
public static String getRouteDescription(ObaRoute route) {
String shortName = route.getShortName();
String longName = route.getLongName();
if (TextUtils.isEmpty(shortName)) {
shortName = longName;
}
if (TextUtils.isEmpty(longName) || shortName.equals(longName)) {
longName = route.getDescription();
}
return UIUtils.formatDisplayText(longName);
}
/**
* Returns a formatted displayText for displaying in the UI for stops, routes, and headsigns, or
* null if the displayText is null. If the displayText IS ALL CAPS and more than one word, it
* will be converted to title case (Is All Caps), otherwise the returned string will match the
* input.
*
* @param displayText displayText to be formatted
* @return formatted text for stop, route, and heasigns for displaying in the UI, or null if the
* displayText is null. If the displayText IS ALL CAPS and more than one word, it will be
* converted to title case (Is All Caps), otherwise the returned string will match the input.
*/
public static String formatDisplayText(String displayText) {
if (displayText == null) {
return null;
}
if (MyTextUtils.isAllCaps(displayText) && displayText.contains(" ")) {
return MyTextUtils.toTitleCase(displayText);
} else {
return displayText;
}
}
// Shows or hides the view, depending on whether or not the direction is
// available.
public static void setStopDirection(View v, String direction, boolean show) {
final TextView text = (TextView) v;
final int directionText = UIUtils.getStopDirectionText(direction);
if ((directionText != R.string.direction_none) || show) {
text.setText(directionText);
text.setVisibility(View.VISIBLE);
} else {
text.setVisibility(View.GONE);
}
}
// Common code to set a route list item view
public static void setRouteView(View view, ObaRoute route) {
TextView shortNameText = (TextView) view.findViewById(R.id.short_name);
TextView longNameText = (TextView) view.findViewById(R.id.long_name);
String shortName = route.getShortName();
String longName = UIUtils.formatDisplayText(route.getLongName());
if (TextUtils.isEmpty(shortName)) {
shortName = longName;
}
if (TextUtils.isEmpty(longName) || shortName.equals(longName)) {
longName = UIUtils.formatDisplayText(route.getDescription());
}
shortNameText.setText(shortName);
longNameText.setText(longName);
}
private static final String[] STOP_USER_PROJECTION = {
ObaContract.Stops._ID,
ObaContract.Stops.FAVORITE,
ObaContract.Stops.USER_NAME
};
public static class StopUserInfoMap {
private final ContentQueryMap mMap;
public StopUserInfoMap(Context context) {
ContentResolver cr = context.getContentResolver();
Cursor c = cr.query(ObaContract.Stops.CONTENT_URI, STOP_USER_PROJECTION, "("
+ ObaContract.Stops.USER_NAME + " IS NOT NULL)" + "OR ("
+ ObaContract.Stops.FAVORITE + "=1)", null, null);
mMap = new ContentQueryMap(c, ObaContract.Stops._ID, true, null);
}
public void close() {
mMap.close();
}
public void requery() {
mMap.requery();
}
public void setView(View stopRoot, String stopId, String stopName) {
TextView nameView = (TextView) stopRoot.findViewById(R.id.stop_name);
setView2(nameView, stopId, stopName, true);
}
/**
* This should be used with compound drawables
*/
public void setView2(TextView nameView, String stopId, String stopName, boolean showIcon) {
ContentValues values = mMap.getValues(stopId);
int icon = 0;
if (values != null) {
Integer i = values.getAsInteger(ObaContract.Stops.FAVORITE);
final boolean favorite = (i != null) && (i == 1);
final String userName = values.getAsString(ObaContract.Stops.USER_NAME);
nameView.setText(TextUtils.isEmpty(userName) ?
UIUtils.formatDisplayText(stopName) : userName);
icon = favorite && showIcon ? R.drawable.ic_toggle_star : 0;
} else {
nameView.setText(UIUtils.formatDisplayText(stopName));
}
nameView.setCompoundDrawablesWithIntrinsicBounds(icon, 0, 0, 0);
}
}
/**
* Returns a comma-delimited list of route display names that serve a stop
* <p/>
* For example, if a stop was served by "14" and "54", this method will return "14,54"
*
* @param stop the stop for which the route display names should be serialized
* @param routes a HashMap containing all routes that serve this stop, with the routeId as the
* key.
* Note that for efficiency this routes HashMap may contain routes that don't
* serve this stop as well -
* the routes for the stop are referenced via stop.getRouteDisplayNames()
* @return comma-delimited list of route display names that serve a stop
*/
public static String serializeRouteDisplayNames(ObaStop stop,
HashMap<String, ObaRoute> routes) {
StringBuffer sb = new StringBuffer();
String[] routeIds = stop.getRouteIds();
for (int i = 0; i < routeIds.length; i++) {
if (routes != null) {
ObaRoute route = routes.get(routeIds[i]);
sb.append(getRouteDisplayName(route));
} else {
// We don't have route mappings - use routeIds
sb.append(routeIds[i]);
}
if (i != routeIds.length - 1) {
sb.append(",");
}
}
return sb.toString();
}
/**
* Returns a list of route display names from a serialized list of route display names
* <p/>
* See {@link #serializeRouteDisplayNames(ObaStop, java.util.HashMap)}
*
* @param serializedRouteDisplayNames comma-separate list of routeIds from serializeRouteDisplayNames()
* @return list of route display names
*/
public static List<String> deserializeRouteDisplayNames(String serializedRouteDisplayNames) {
String routes[] = serializedRouteDisplayNames.split(",");
return Arrays.asList(routes);
}
/**
* Returns a formatted and sorted list of route display names for presentation in a single line
* <p/>
* For example, the following list:
* <p/>
* 11,1,15, 8b
* <p/>
* ...would be formatted as:
* <p/>
* 4, 8b, 11, 15
*
* @param routeDisplayNames list of route display names
* @param nextArrivalRouteShortNames the short route names of the next X arrivals at the stop
* that are the same. These will be highlighted in the
* results.
* @return a formatted and sorted list of route display names for presentation in a single line
*/
public static String formatRouteDisplayNames(List<String> routeDisplayNames,
List<String> nextArrivalRouteShortNames) {
Collections.sort(routeDisplayNames, new AlphanumComparator());
StringBuffer sb = new StringBuffer();
for (int i = 0; i < routeDisplayNames.size(); i++) {
boolean match = false;
for (String nextArrivalRouteShortName : nextArrivalRouteShortNames) {
if (routeDisplayNames.get(i).equalsIgnoreCase(nextArrivalRouteShortName)) {
match = true;
break;
}
}
if (match) {
// If this route name matches a route name for the next X arrivals that are the same, highlight this route in the text
sb.append(routeDisplayNames.get(i) + "*");
} else {
// Just append the normally-formatted route name
sb.append(routeDisplayNames.get(i));
}
if (i != routeDisplayNames.size() - 1) {
sb.append(", ");
}
}
return sb.toString();
}
/**
* Generates the dialog text that is used to show detailed information about a particular stop
*
* @return a pair of Strings consisting of the <dialog title, dialog message>
*/
public static Pair<String, String> createStopDetailsDialogText(Context context, String stopName,
String stopUserName, String stopCode, String stopDirection,
List<String> routeDisplayNames) {
final String newLine = "\n";
String title = "";
StringBuilder message = new StringBuilder();
if (!TextUtils.isEmpty(stopUserName)) {
title = stopUserName;
if (stopName != null) {
// Show official stop name in addition to user name
message.append(
context.getString(R.string.stop_info_official_stop_name_label, stopName))
.append(newLine);
}
} else if (stopName != null) {
title = stopName;
}
if (stopCode != null) {
message.append(context.getString(R.string.stop_details_code, stopCode) + newLine);
}
// Routes that serve this stop
if (routeDisplayNames != null) {
String routes = context.getString(R.string.stop_info_route_ids_label) + " " + UIUtils
.formatRouteDisplayNames(routeDisplayNames, new ArrayList<String>());
message.append(routes);
}
if (!TextUtils.isEmpty(stopDirection)) {
message.append(newLine)
.append(context.getString(UIUtils.getStopDirectionText(stopDirection)));
}
return new Pair(title, message.toString());
}
/**
* Builds an AlertDialog with the given title and message
*
* @return an AlertDialog with the given title and message
*/
public static AlertDialog buildAlertDialog(Context context, String title, String message) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(title);
builder.setMessage(message);
return builder.create();
}
/**
* Default implementation for creating a shortcut when in shortcut mode.
*
* @param name The name of the shortcut.
* @param destIntent The destination intent.
*/
public static Intent makeShortcut(Context context, String name, Intent destIntent) {
// Make sure the shortcut Activity always launches on top (#626)
destIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
// Set up the container intent
Intent intent = new Intent();
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, destIntent);
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
Parcelable iconResource = Intent.ShortcutIconResource
.fromContext(context, R.mipmap.ic_launcher);
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource);
return intent;
}
public static void goToUrl(Context context, String url) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, context.getString(R.string.browser_error), Toast.LENGTH_SHORT)
.show();
}
}
public static void goToPhoneDialer(Context context, String url) {
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse(url));
context.startActivity(intent);
}
/**
* Opens email apps based on the given email address
* @param email address
* @param location string that shows the current location
*/
public static void sendEmail(Context context, String email, String location) {
sendEmail(context, email, location, null, false);
}
/**
* Opens email apps based on the given email address
* @param email address
* @param location string that shows the current location
* @param tripPlanUrl trip planning URL that failed, if this is a trip problem error report, or null if it's not
*/
public static void sendEmail(Context context, String email, String location,
String tripPlanUrl, boolean tripPlanFail) {
String obaRegionName = RegionUtils.getObaRegionName();
boolean autoRegion = Application.getPrefs()
.getBoolean(context.getString(R.string.preference_key_auto_select_region), true);
String regionSelectionMethod;
if (autoRegion) {
regionSelectionMethod = context.getString(R.string.region_selected_auto);
} else {
regionSelectionMethod = context.getString(R.string.region_selected_manually);
}
UIUtils.sendEmail(context, email, location, obaRegionName, regionSelectionMethod,
tripPlanUrl, tripPlanFail);
}
/**
* Opens email apps based on the given email address
* @param email address
* @param location string that shows the current location
* @param regionName name of the current api region
* @param regionSelectionMethod string that shows if the current api region selected manually or
* automatically
* @param tripPlanUrl trip planning URL that failed, if this is a trip problem error report, or null if it's not
*/
private static void sendEmail(Context context, String email, String location, String regionName,
String regionSelectionMethod, String tripPlanUrl, boolean tripPlanFail) {
PackageManager pm = context.getPackageManager();
PackageInfo appInfoOba;
PackageInfo appInfoGps;
String obaVersion = "";
String googlePlayServicesAppVersion = "";
try {
appInfoOba = pm.getPackageInfo(context.getPackageName(),
PackageManager.GET_META_DATA);
obaVersion = appInfoOba.versionName;
} catch (PackageManager.NameNotFoundException e) {
// Leave version as empty string
}
try {
appInfoGps = pm.getPackageInfo(GoogleApiAvailability.GOOGLE_PLAY_SERVICES_PACKAGE, 0);
googlePlayServicesAppVersion = appInfoGps.versionName;
} catch (PackageManager.NameNotFoundException e) {
// Leave version as empty string
}
String body;
if (location != null) {
// Have location
if (tripPlanUrl == null) {
// No trip plan
body = context.getString(R.string.bug_report_body,
obaVersion,
Build.MODEL,
Build.VERSION.RELEASE,
Build.VERSION.SDK_INT,
googlePlayServicesAppVersion,
GoogleApiAvailability.GOOGLE_PLAY_SERVICES_VERSION_CODE,
regionName,
regionSelectionMethod,
location);
} else {
// Trip plan
if (tripPlanFail) {
body = context.getString(R.string.bug_report_body_trip_plan_fail,
obaVersion,
Build.MODEL,
Build.VERSION.RELEASE,
Build.VERSION.SDK_INT,
googlePlayServicesAppVersion,
GoogleApiAvailability.GOOGLE_PLAY_SERVICES_VERSION_CODE,
regionName,
regionSelectionMethod,
location,
tripPlanUrl);
} else {
body = context.getString(R.string.bug_report_body_trip_plan,
obaVersion,
Build.MODEL,
Build.VERSION.RELEASE,
Build.VERSION.SDK_INT,
googlePlayServicesAppVersion,
GoogleApiAvailability.GOOGLE_PLAY_SERVICES_VERSION_CODE,
regionName,
regionSelectionMethod,
location,
tripPlanUrl);
}
}
} else {
// No location
if (tripPlanUrl == null) {
// No trip plan
body = context.getString(R.string.bug_report_body_without_location,
obaVersion,
Build.MODEL,
Build.VERSION.RELEASE,
Build.VERSION.SDK_INT);
} else {
// Trip plan
if (tripPlanFail) {
body = context.getString(R.string.bug_report_body_trip_plan_without_location_fail,
obaVersion,
Build.MODEL,
Build.VERSION.RELEASE,
Build.VERSION.SDK_INT,
tripPlanUrl);
} else {
body = context.getString(R.string.bug_report_body_trip_plan_without_location,
obaVersion,
Build.MODEL,
Build.VERSION.RELEASE,
Build.VERSION.SDK_INT,
tripPlanUrl);
}
}
}
Intent send = new Intent(Intent.ACTION_SEND);
send.putExtra(Intent.EXTRA_EMAIL,
new String[]{email});
// Show trip planner subject line if we have a trip planning URL
String subject;
if (tripPlanUrl == null) {
if (tripPlanFail) {
subject = context.getString(R.string.bug_report_subject_trip_plan);
} else {
subject = context.getString(R.string.bug_report_subject);
}
} else {
if (tripPlanFail) {
subject = context.getString(R.string.bug_report_subject_trip_plan_fail);
} else {
subject = context.getString(R.string.bug_report_subject_trip_plan);
}
}
send.putExtra(Intent.EXTRA_SUBJECT, subject);
send.putExtra(Intent.EXTRA_TEXT, body);
send.setType("message/rfc822");
try {
context.startActivity(Intent.createChooser(send, subject));
} catch (ActivityNotFoundException e) {
Toast.makeText(context, R.string.bug_report_error, Toast.LENGTH_LONG)
.show();
}
}
public static String getRouteErrorString(Context context, int code) {
if (!isConnected(context)) {
if (isAirplaneMode(context)) {
return context.getString(R.string.airplane_mode_error);
} else {
return context.getString(R.string.no_network_error);
}
}
switch (code) {
case ObaApi.OBA_INTERNAL_ERROR:
return context.getString(R.string.internal_error);
case ObaApi.OBA_NOT_FOUND:
ObaRegion r = Application.get().getCurrentRegion();
if (r != null) {
return context.getString(R.string.route_not_found_error_with_region_name,
r.getName());
} else {
return context.getString(R.string.route_not_found_error_no_region);
}
case ObaApi.OBA_BAD_GATEWAY:
return context.getString(R.string.bad_gateway_error);
case ObaApi.OBA_OUT_OF_MEMORY:
return context.getString(R.string.out_of_memory_error);
default:
return context.getString(R.string.generic_comm_error);
}
}
public static String getStopErrorString(Context context, int code) {
if (!isConnected(context)) {
if (isAirplaneMode(context)) {
return context.getString(R.string.airplane_mode_error);
} else {
return context.getString(R.string.no_network_error);
}
}
switch (code) {
case ObaApi.OBA_INTERNAL_ERROR:
return context.getString(R.string.internal_error);
case ObaApi.OBA_NOT_FOUND:
ObaRegion r = Application.get().getCurrentRegion();
if (r != null) {
return context
.getString(R.string.stop_not_found_error_with_region_name, r.getName());
} else {
return context.getString(R.string.stop_not_found_error_no_region);
}
case ObaApi.OBA_BAD_GATEWAY:
return context.getString(R.string.bad_gateway_error);
case ObaApi.OBA_OUT_OF_MEMORY:
return context.getString(R.string.out_of_memory_error);
default:
return context.getString(R.string.generic_comm_error);
}
}
/**
* Returns the resource ID for a user-friendly error message based on device state (if a
* network
* connection is available or airplane mode is on) or an OBA REST API response code
*
* @param code The status code (one of the ObaApi.OBA_* constants)
* @return the resource ID for a user-friendly error message based on device state (if a network
* connection is available or airplane mode is on) or an OBA REST API response code
*/
public static int getMapErrorString(Context context, int code) {
if (!isConnected(context)) {
if (isAirplaneMode(context)) {
return R.string.airplane_mode_error;
} else {
return R.string.no_network_error;
}
}
switch (code) {
case ObaApi.OBA_INTERNAL_ERROR:
return R.string.internal_error;
case ObaApi.OBA_BAD_GATEWAY:
return R.string.bad_gateway_error;
case ObaApi.OBA_OUT_OF_MEMORY:
return R.string.out_of_memory_error;
default:
return R.string.map_generic_error;
}
}
/**
* Returns true if the device is in Airplane Mode, and false if the device isn't in Airplane
* mode or if it can't be determined
* @param context
* @return true if the device is in Airplane Mode, and false if the device isn't in Airplane
* mode or if it can't be determined
*/
public static boolean isAirplaneMode(Context context) {
if (context == null) {
// If the context is null, we can't get airplane mode state - assume no
return false;
}
ContentResolver cr = context.getContentResolver();
return Settings.System.getInt(cr, Settings.System.AIRPLANE_MODE_ON, 0) != 0;
}
/**
* Returns true if the device is connected to a network, and false if the device isn't or if it
* can't be determined
* @param context
* @return true if the device is connected to a network, and false if the device isn't or if it
* can't be determined
*/
public static boolean isConnected(Context context) {
if (context == null) {
// If the context is null, we can't get connected state - assume yes
return true;
}
ConnectivityManager cm =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
return (activeNetwork != null) && activeNetwork.isConnectedOrConnecting();
}
/**
* Returns the first string for the query URI.
*/
public static String stringForQuery(Context context, Uri uri, String column) {
ContentResolver cr = context.getContentResolver();
Cursor c = cr.query(uri, new String[]{column}, null, null, null);
if (c != null) {
try {
if (c.moveToFirst()) {
return c.getString(0);
}
} finally {
c.close();
}
}
return "";
}
public static Integer intForQuery(Context context, Uri uri, String column) {
ContentResolver cr = context.getContentResolver();
Cursor c = cr.query(uri, new String[]{column}, null, null, null);
if (c != null) {
try {
if (c.moveToFirst()) {
return c.getInt(0);
}
} finally {
c.close();
}
}
return null;
}
public static final int MINUTES_IN_HOUR = 60;
/**
* Takes the number of minutes, and returns a user-readable string
* saying the number of minutes in which no arrivals are coming,
* or the number of hours and minutes if minutes if minutes > 60
*
* @param minutes number of minutes for which there are no upcoming arrivals
* @param additionalArrivals true if the response should include the word additional, false if
* it should not
* @param shortFormat true if the format should be abbreviated, false if it should be
* long
* @return a user-readable string saying the number of minutes in which no arrivals are coming,
* or the number of hours and minutes if minutes > 60
*/
public static String getNoArrivalsMessage(Context context, int minutes,
boolean additionalArrivals, boolean shortFormat) {
if (minutes <= MINUTES_IN_HOUR) {
// Return just minutes
if (additionalArrivals) {
if (shortFormat) {
// Abbreviated version
return context
.getString(R.string.stop_info_no_additional_data_minutes_short_format,
minutes);
} else {
// Long version
return context
.getString(R.string.stop_info_no_additional_data_minutes, minutes);
}
} else {
if (shortFormat) {
// Abbreviated version
return context
.getString(R.string.stop_info_nodata_minutes_short_format, minutes);
} else {
// Long version
return context.getString(R.string.stop_info_nodata_minutes, minutes);
}
}
} else {
// Return hours and minutes
if (additionalArrivals) {
if (shortFormat) {
// Abbreviated version
return context.getResources()
.getQuantityString(
R.plurals.stop_info_no_additional_data_hours_minutes_short_format,
minutes / 60, minutes % 60, minutes / 60);
} else {
// Long version
return context.getResources()
.getQuantityString(R.plurals.stop_info_no_additional_data_hours_minutes,
minutes / 60, minutes % 60, minutes / 60);
}
} else {
if (shortFormat) {
// Abbreviated version
return context.getResources()
.getQuantityString(
R.plurals.stop_info_nodata_hours_minutes_short_format,
minutes / 60,
minutes % 60, minutes / 60);
} else {
// Long version
return context.getResources()
.getQuantityString(R.plurals.stop_info_nodata_hours_minutes,
minutes / 60,
minutes % 60, minutes / 60);
}
}
}
}
/**
* Returns true if the activity is still active and dialogs can be managed (i.e., displayed
* or dismissed), or false if it is
* not
*
* @param activity Activity to check for displaying/dismissing a dialog
* @return true if the activity is still active and dialogs can be managed, or false if it is
* not
*/
public static boolean canManageDialog(Activity activity) {
if (activity == null) {
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return !activity.isFinishing() && !activity.isDestroyed();
} else {
return !activity.isFinishing();
}
}
/**
* Returns true if the context is an Activity and is still active and dialogs can be managed
* (i.e., displayed or dismissed) OR the context is not an Activity, or false if the Activity
* is
* no longer active.
*
* NOTE: We really shouldn't display dialogs from a Service - a notification is a better way
* to communicate with the user.
*
* @param context Context to check for displaying/dismissing a dialog
* @return true if the context is an Activity and is still active and dialogs can be managed
* (i.e., displayed or dismissed) OR the context is not an Activity, or false if the Activity
* is
* no longer active
*/
public static boolean canManageDialog(Context context) {
if (context == null) {
return false;
}
if (context instanceof Activity) {
return canManageDialog((Activity) context);
} else {
// We really shouldn't be displaying dialogs from a Service, but if for some reason we
// need to do this, we don't have any way of checking whether its possible
return true;
}
}
/**
* Returns true if the API level supports animating Views using ViewPropertyAnimator, false if
* it doesn't
*
* @return true if the API level supports animating Views using ViewPropertyAnimator, false if
* it doesn't
*/
public static boolean canAnimateViewModern() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1;
}
/**
* Returns true if the API level supports canceling existing animations via the
* ViewPropertyAnimator, and false if it does not
*
* @return true if the API level supports canceling existing animations via the
* ViewPropertyAnimator, and false if it does not
*/
public static boolean canCancelAnimation() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
}
/**
* Returns true if the API level supports our Arrival Info Style B (sort by route) views, false
* if it does not. See #350 and #275.
*
* @return true if the API level supports our Arrival Info Style B (sort by route) views, false
* if it does not
*/
public static boolean canSupportArrivalInfoStyleB() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
}
/**
* Shows a view, using animation if the platform supports it
*
* @param v View to show
* @param animationDuration duration of animation
*/
@TargetApi(14)
public static void showViewWithAnimation(final View v, int animationDuration) {
// If we're on a legacy device, show the view without the animation
if (!canAnimateViewModern()) {
showViewWithoutAnimation(v);
return;
}
if (v.getVisibility() == View.VISIBLE && v.getAlpha() == 1) {
// View is already visible and not transparent, return without doing anything
return;
}
v.clearAnimation();
if (canCancelAnimation()) {
v.animate().cancel();
}
if (v.getVisibility() != View.VISIBLE) {
// Set the content view to 0% opacity but visible, so that it is visible
// (but fully transparent) during the animation.
v.setAlpha(0f);
v.setVisibility(View.VISIBLE);
}
// Animate the content view to 100% opacity, and clear any animation listener set on the view.
v.animate()
.alpha(1f)
.setDuration(animationDuration)
.setListener(null);
}
/**
* Shows a view without using animation
*
* @param v View to show
*/
public static void showViewWithoutAnimation(final View v) {
if (v.getVisibility() == View.VISIBLE) {
// View is already visible, return without doing anything
return;
}
v.setVisibility(View.VISIBLE);
}
/**
* Hides a view, using animation if the platform supports it
*
* @param v View to hide
* @param animationDuration duration of animation
*/
@TargetApi(14)
public static void hideViewWithAnimation(final View v, int animationDuration) {
// If we're on a legacy device, hide the view without the animation
if (!canAnimateViewModern()) {
hideViewWithoutAnimation(v);
return;
}
if (v.getVisibility() == View.GONE) {
// View is already gone, return without doing anything
return;
}
v.clearAnimation();
if (canCancelAnimation()) {
v.animate().cancel();
}
// Animate the view to 0% opacity. After the animation ends, set its visibility to GONE as
// an optimization step (it won't participate in layout passes, etc.)
v.animate()
.alpha(0f)
.setDuration(animationDuration)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
v.setVisibility(View.GONE);
}
});
}
/**
* Hides a view without using animation
*
* @param v View to hide
*/
public static void hideViewWithoutAnimation(final View v) {
if (v.getVisibility() == View.GONE) {
// View is already gone, return without doing anything
return;
}
// Hide the view without animation
v.setVisibility(View.GONE);
}
/**
* Prints View visibility information to the log for debugging purposes
*
* @param v View to log visibility information for
*/
@TargetApi(12)
public static void logViewVisibility(View v) {
if (v != null) {
if (v.getVisibility() == View.VISIBLE) {
Log.d(TAG, v.getContext().getResources().getResourceEntryName(v.getId())
+ " is visible");
if (UIUtils.canAnimateViewModern()) {
Log.d(TAG, v.getContext().getResources().getResourceEntryName(v.getId())
+ " alpha - " + v.getAlpha());
}
} else if (v.getVisibility() == View.INVISIBLE) {
Log.d(TAG, v.getContext().getResources().getResourceEntryName(v.getId())
+ " is INVISIBLE");
} else if (v.getVisibility() == View.GONE) {
Log.d(TAG,
v.getContext().getResources().getResourceEntryName(v.getId()) + " is GONE");
} else {
Log.d(TAG, v.getContext().getResources().getResourceEntryName(v.getId())
+ ".getVisibility() - " + v.getVisibility());
}
}
}
/**
* Converts screen dimension units from dp to pixels, based on algorithm defined in
* http://developer.android.com/guide/practices/screens_support.html#dips-pels
*
* @param dp value in dp
* @return value in pixels
*/
public static int dpToPixels(Context context, float dp) {
// Get the screen's density scale
final float scale = context.getResources().getDisplayMetrics().density;
// Convert the dps to pixels, based on density scale
return (int) (dp * scale + 0.5f);
}
/**
* Sets the margins for a given view
*
* @param v View to set the margin for
* @param l left margin, in pixels
* @param t top margin, in pixels
* @param r right margin, in pixels
* @param b bottom margin, in pixels
*/
public static void setMargins(View v, int l, int t, int r, int b) {
ViewGroup.MarginLayoutParams p = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
p.setMargins(l, t, r, b);
v.setLayoutParams(p);
}
/**
* Formats a view so it is ignored for accessible access
*/
public static void setAccessibilityIgnore(View view) {
view.setClickable(false);
view.setFocusable(false);
view.setContentDescription("");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
}
/**
* Builds the list of Strings that should be shown for a given trip "Bus Options" menu,
* provided the arguments for that trip
*
* @param c Context
* @param isRouteFavorite true if this route is a user favorite, false if it is not
* @param hasUrl true if the route provides a URL for schedule data, false if it does
* not
* @param isReminderVisible true if the reminder is currently visible for a trip, false if it
* is
* not
* @return the list of Strings that should be shown for a given trip, provided the arguments for
* that trip
*/
public static List<String> buildTripOptions(Context c, boolean isRouteFavorite, boolean hasUrl,
boolean isReminderVisible) {
ArrayList<String> list = new ArrayList<>();
if (!isRouteFavorite) {
list.add(c.getString(R.string.bus_options_menu_add_star));
} else {
list.add(c.getString(R.string.bus_options_menu_remove_star));
}
list.add(c.getString(R.string.bus_options_menu_show_route_on_map));
list.add(c.getString(R.string.bus_options_menu_show_trip_details));
if (!isReminderVisible) {
list.add(c.getString(R.string.bus_options_menu_set_reminder));
} else {
list.add(c.getString(R.string.bus_options_menu_edit_reminder));
}
list.add(c.getString(R.string.bus_options_menu_show_only_this_route));
if (hasUrl) {
list.add(c.getString(R.string.bus_options_menu_show_route_schedule));
}
list.add(c.getString(R.string.bus_options_menu_report_trip_problem));
return list;
}
/**
* Builds the array of icons that should be shown for the trip "Bus Options" menu, given the
* provided arguments for that trip
*
* @param isRouteFavorite true if this route is a user favorite, false if it is not
* @param hasUrl true if the route provides a URL for schedule data, false if it does
* not
* @return the array of icons that should be shown for a given trip
*/
public static List<Integer> buildTripOptionsIcons(boolean isRouteFavorite, boolean hasUrl) {
ArrayList<Integer> list = new ArrayList<>();
if (!isRouteFavorite) {
list.add(R.drawable.focus_star_on);
} else {
list.add(R.drawable.focus_star_off);
}
list.add(R.drawable.ic_arrivals_styleb_action_map);
list.add(R.drawable.ic_trip_details);
list.add(R.drawable.ic_drawer_alarm);
list.add(R.drawable.ic_content_filter_list);
if (hasUrl) {
list.add(R.drawable.ic_notification_event_note);
}
list.add(R.drawable.ic_alert_warning);
return list;
}
/**
* Sets the line and fill colors for real-time indicator circles contained in the provided
* realtime_indicator.xml layout. There are several circles, so each needs to be set
* individually. The resource code for the color to be used should be provided.
*
* @param vg realtime_indicator.xml layout
* @param lineColor resource code color to be used as line color, or null to use the default
* colors
* @param fillColor resource code color to be used as fill color, or null to use the default
* colors
*/
public static void setRealtimeIndicatorColorByResourceCode(ViewGroup vg, Integer lineColor,
Integer fillColor) {
Resources r = vg.getResources();
setRealtimeIndicatorColor(vg, r.getColor(lineColor), r.getColor(fillColor));
}
/**
* Sets the line and fill colors for real-time indicator circles contained in the provided
* realtime_indicator.xml layout. There are several circles, so each needs to be set
* individually. The integer representation of the color to be used should be provided.
*
* @param vg realtime_indicator.xml layout
* @param lineColor color to be used as line color, or null to use the default colors
* @param fillColor color to be used as fill color, or null to use the default colors
*/
public static void setRealtimeIndicatorColor(ViewGroup vg, Integer lineColor,
Integer fillColor) {
for (int i = 0; i < vg.getChildCount(); i++) {
View v = vg.getChildAt(i);
if (v instanceof RealtimeIndicatorView) {
if (lineColor != null) {
((RealtimeIndicatorView) v).setLineColor(lineColor);
} else {
// Use default color
((RealtimeIndicatorView) v).setLineColor(
R.color.realtime_indicator_line);
}
if (fillColor != null) {
((RealtimeIndicatorView) v).setFillColor(fillColor);
} else {
// Use default color
((RealtimeIndicatorView) v).setLineColor(
R.color.realtime_indicator_fill);
}
}
}
}
/**
* Creates a new Bitmap, with the black color of the source image changed to the given color.
* The source Bitmap isn't modified.
*
* @param source the source Bitmap with a black background
* @param color the color to change the black color to
* @return the resulting Bitmap that has the black changed to the color
*/
public static Bitmap colorBitmap(Bitmap source, int color) {
int width = source.getWidth();
int height = source.getHeight();
int[] pixels = new int[width * height];
source.getPixels(pixels, 0, width, 0, 0, width, height);
for (int x = 0; x < pixels.length; ++x) {
pixels[x] = (pixels[x] == Color.BLACK) ? color : pixels[x];
}
Bitmap out = Bitmap.createBitmap(width, height, source.getConfig());
out.setPixels(pixels, 0, width, 0, 0, width, height);
return out;
}
/**
* Returns true if the provided touch event was within the provided view
*
* @return true if the provided touch event was within the provided view
*/
public static boolean isTouchInView(View view, MotionEvent event) {
Rect rect = new Rect();
view.getGlobalVisibleRect(rect);
return rect.contains((int) event.getRawX(), (int) event.getRawY());
}
/**
* Returns the current time for comparison against another current time. For API levels >=
* Jelly Bean MR1 the SystemClock.getElapsedRealtimeNanos() method is used, and for API levels
* <
* Jelly Bean MR1 System.currentTimeMillis() is used.
*
* @return the current time for comparison against another current time, in nanoseconds
*/
public static long getCurrentTimeForComparison() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
// Use elapsed real-time nanos, since its guaranteed monotonic
return SystemClock.elapsedRealtimeNanos();
} else {
return TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis());
}
}
/**
* Open the soft keyboard
*/
public static void openKeyboard(Context context) {
InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(
Context.INPUT_METHOD_SERVICE);
inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED,
InputMethodManager.HIDE_NOT_ALWAYS);
}
/**
* Closes the soft keyboard
*/
public static void closeKeyboard(Context context, View v) {
InputMethodManager imm =
(InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
}
/**
* Returns a list of all situations (service alerts) that are specific to the stop, routes, and
* agency
* for the provided arrivals-and-departures-for-stop response. For route-specific alerts, this
* involves looping through the routes and checking the references element to see if there are
* any route-specific alerts, and adding them to the list to be shown above the list of
* arrivals
* for a stop. See #700.
*
* @param response response from arrivals-and-departures-for-stop API
* @return a list of all situations (service alerts) that are specific to the stop, routes, and
* agency
* for the provided arrivals-and-departures-for-stop response. See #700.
*/
public static List<ObaSituation> getAllSituations(final ObaArrivalInfoResponse response) {
List<ObaSituation> allSituations = new ArrayList<>();
// Add agency-wide and stop-specific alerts
allSituations.addAll(response.getSituations());
// Add all existing Ids to a HashSet for O(1) retrieval (vs. list)
HashSet<String> allIds = new HashSet<>();
for (ObaSituation s : allSituations) {
allIds.add(s.getId());
}
// Scan through the routes, and if a route-specific situation hasn't been added yet, add it
ObaArrivalInfo[] info = response.getArrivalInfo();
for (ObaArrivalInfo i : info) {
for (String situationId : i.getSituationIds()) {
if (!allIds.contains(situationId)) {
allIds.add(situationId);
allSituations.add(response.getSituation(situationId));
}
}
}
return allSituations;
}
/**
* Returns true if the provided currentTime falls within the situation's (i.e., alert's) active
* windows or if the situation does not provide an active window, and false if the currentTime
* falls outside of the situation's active windows
*
* @param currentTime the time to compare to the situation's windows, in milliseconds between
* the current time and midnight, January 1, 1970 UTC
* @return true if the provided currentTime falls within the situation's (i.e., alert's) active
* windows or if the situation does not provide an active window, and false if the currentTime
* falls outside of the situation's active windows
*/
public static boolean isActiveWindowForSituation(ObaSituation situation, long currentTime) {
if (situation.getActiveWindows().length == 0) {
// We assume a situation is active if it doesn't contain any active window information
return true;
}
// Active window times are in seconds since epoch
long currentTimeSeconds = TimeUnit.MILLISECONDS.toSeconds(currentTime);
boolean isActiveWindowForSituation = false;
for (ObaSituation.ActiveWindow activeWindow : situation.getActiveWindows()) {
long from = activeWindow.getFrom();
long to = activeWindow.getTo();
if (from <= currentTimeSeconds && currentTimeSeconds <= to) {
isActiveWindowForSituation = true;
break;
}
}
return isActiveWindowForSituation;
}
/**
* Returns the time formatting as "1:10pm" to be displayed as an absolute time for an
* arrival/departure
*
* @param time an arrival or departure time (e.g., from ArrivalInfo)
* @return the time formatting as "1:10pm" to be displayed as an absolute time for an
* arrival/departure
*/
public static String formatTime(Context context, long time) {
return DateUtils.formatDateTime(context,
time,
DateUtils.FORMAT_SHOW_TIME |
DateUtils.FORMAT_NO_NOON |
DateUtils.FORMAT_NO_MIDNIGHT
);
}
/**
* Set smaller text size if the route short name has more than 3 characters
*
* @param view Text view
* @param routeShortName Route short name
*/
public static void maybeShrinkRouteName(Context context, TextView view, String routeShortName) {
if (routeShortName.length() < 4) {
// No-op if text is short enough to fit
return;
} else if (routeShortName.length() == 4) {
view.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources().
getDimension(R.dimen.route_name_text_size_medium));
} else if (routeShortName.length() > 4) {
view.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources().
getDimension(R.dimen.route_name_text_size_small));
}
}
/**
* Transforms a given opaque color into the same color but with the given alpha value
*
* @param solidColor hex color value that is completely opaque
* @param alpha Specify an alpha value. 0 means fully transparent, and 255 means fully
* opaque.
* @return the provided color with the given alpha value
*/
public static int getTransparentColor(int solidColor, int alpha) {
int r = Color.red(solidColor);
int g = Color.green(solidColor);
int b = Color.blue(solidColor);
return Color.argb(alpha, r, g, b);
}
/**
* Returns the location of the map center if it has been previously saved in the bundle, or
* null if it wasn't saved in the bundle.
*
* @param b bundle to check for the map center
* @return the location of the map center if it has been previously saved in the bundle, or null
* if it wasn't saved in the bundle.
*/
public static Location getMapCenter(Bundle b) {
if (b == null) {
return null;
}
Location center = null;
double lat = b.getDouble(MapParams.CENTER_LAT);
double lon = b.getDouble(MapParams.CENTER_LON);
if (lat != 0.0 && lon != 0.0) {
center = LocationUtils.makeLocation(lat, lon);
}
return center;
}
/**
* Creates a JPEG image file with the current date/time as the name
*
* @param nameSuffix A string that will be added to the end of the file name, or null if
* nothing
* should be added
* @return a JPEG image file with the current date/time as the name
*/
public static File createImageFile(Context context, String nameSuffix) throws IOException {
// Create an image file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
StringBuilder imageFileName = new StringBuilder();
imageFileName.append("JPEG_");
imageFileName.append(timeStamp);
imageFileName.append("_");
if (nameSuffix != null) {
imageFileName.append(nameSuffix);
}
File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
return File.createTempFile(
imageFileName.toString(), /* prefix */
".jpg", /* suffix */
storageDir /* directory */
);
}
/**
* Decode a smaller sampled bitmap given a large bitmap.
* Adapted from https://developer.android.com/training/displaying-bitmaps/load-bitmap.html and
* http://stackoverflow.com/a/31720143/937715.
*
* @param pathName path to the full size image file
* @param reqWidth desired width
* @param reqHeight desired height
* @return a smaller version of the image at pathName, given the desired width and height
*/
public static Bitmap decodeSampledBitmapFromFile(String pathName, int reqWidth, int reqHeight)
throws IOException {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(pathName, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
Bitmap b = BitmapFactory.decodeFile(pathName, options);
return rotateImageIfRequired(b, pathName);
}
/**
* Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding
* bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates
* the closest inSampleSize that will result in the final decoded bitmap having a width and
* height equal to or larger than the requested width and height. This implementation does not
* ensure a power of 2 is returned for inSampleSize which can be faster when decoding but
* results in a larger bitmap which isn't as useful for caching purposes.
*
* From http://stackoverflow.com/a/31720143/937715.
*
* @param options An options object with out* params already populated (run through a decode*
* method with inJustDecodeBounds==true
* @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap
* @return The value to be used for inSampleSize
*/
private 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) {
// Calculate ratios of height and width to requested height and width
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
// Choose the smallest ratio as inSampleSize value, this will guarantee a final image
// with both dimensions larger than or equal to the requested height and width.
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
// This offers some additional logic in case the image has a strange
// aspect ratio. For example, a panorama may have a much larger
// width than height. In these cases the total pixels might still
// end up being too large to fit comfortably in memory, so we should
// be more aggressive with sample down the image (=larger inSampleSize).
final float totalPixels = width * height;
// Anything more than 2x the requested pixels we'll sample down further
final float totalReqPixelsCap = reqWidth * reqHeight * 2;
while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
inSampleSize++;
}
}
return inSampleSize;
}
/**
* Rotate an image if required.
*
* @param img The image bitmap
* @param imagePath Path to image
* @return The resulted Bitmap after manipulation
*/
private static Bitmap rotateImageIfRequired(Bitmap img, String imagePath) throws IOException {
ExifInterface ei = new ExifInterface(imagePath);
int orientation = ei
.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
return rotateImage(img, 90);
case ExifInterface.ORIENTATION_ROTATE_180:
return rotateImage(img, 180);
case ExifInterface.ORIENTATION_ROTATE_270:
return rotateImage(img, 270);
default:
return img;
}
}
/**
* Rotate the given bitmap
*
* @param img image to rotate
* @param degree number of degrees to rotate, from 0-360
* @return the provided bitmap rotated by the given number of degrees
*/
private static Bitmap rotateImage(Bitmap img, int degree) {
Matrix matrix = new Matrix();
matrix.postRotate(degree);
Bitmap rotatedImg = Bitmap
.createBitmap(img, 0, 0, img.getWidth(), img.getHeight(), matrix, true);
img.recycle();
return rotatedImg;
}
}