org.physical_web.physicalweb.Utils.java Source code

Java tutorial

Introduction

Here is the source code for org.physical_web.physicalweb.Utils.java

Source

/*
 * Copyright 2016 Google Inc. All rights reserved.
 *
 * 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.physical_web.physicalweb;

import org.physical_web.collection.PhysicalWebCollection;
import org.physical_web.collection.PwPair;
import org.physical_web.collection.PwsClient;
import org.physical_web.collection.PwsResult;
import org.physical_web.collection.UrlDevice;

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.view.Menu;
import android.widget.Toast;

import org.uribeacon.scan.util.RangingUtils;
import org.uribeacon.scan.util.RegionResolver;

import org.json.JSONException;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;

/**
 * This class is for various static utilities, largely for manipulation of data structures provided
 * by the collections library.
 */
class Utils {
    public static final String PROD_ENDPOINT = "https://url-caster.appspot.com";
    public static final int PROD_ENDPOINT_VERSION = 1;
    public static final String DEV_ENDPOINT = "https://url-caster-dev.appspot.com";
    public static final int DEV_ENDPOINT_VERSION = 1;
    public static final String GOOGLE_ENDPOINT = "https://physicalweb.googleapis.com";
    public static final int GOOGLE_ENDPOINT_VERSION = 2;
    public static final String FAVORITES_KEY = "favorites";
    public static final String BLE_DEVICE_TYPE = "ble";
    public static final String FAT_BEACON_DEVICE_TYPE = "fat-beacon";
    public static final String MDNS_LOCAL_DEVICE_TYPE = "mdns-local";
    public static final String MDNS_PUBLIC_DEVICE_TYPE = "mdns-public";
    public static final String WIFI_DIRECT_DEVICE_TYPE = "wifidirect";
    public static final String SSDP_DEVICE_TYPE = "ssdp";
    private static final String USER_OPTED_IN_KEY = "userOptedIn";
    private static final String MAIN_PREFS_KEY = "physical_web_preferences";
    private static final String DISCOVERY_SERVICE_PREFS_KEY = "org.physical_web.physicalweb.DISCOVERY_SERVICE_PREFS";
    private static final String SCANTIME_KEY = "scantime";
    private static final String TYPE_KEY = "type";
    private static final String PUBLIC_KEY = "public";
    private static final String TITLE_KEY = "title";
    private static final String DESCRIPTION_KEY = "description";
    private static final String RSSI_KEY = "rssi";
    private static final String TXPOWER_KEY = "tx";
    private static final String PWSTRIPTIME_KEY = "pwstriptime";
    private static final String WIFIDIRECT_KEY = "wifidirect";
    private static final String WIFIDIRECT_PORT_KEY = "wifiport";
    private static final RegionResolver REGION_RESOLVER = new RegionResolver();
    private static final String SEPARATOR = "\0";
    private static Set<String> mFavoriteUrls = new HashSet<>();
    private static Set<String> mBlockedUrls = new HashSet<>();
    private static final int GZIP_SIGNATURE_LENGTH = 2;

    // Compares PwPairs by first considering if it has been favorited
    // and then considering distance
    public static class PwPairRelevanceComparator implements Comparator<PwPair> {
        private Set<String> mFavorites = mFavoriteUrls;
        public Map<String, Double> mCachedDistances;

        PwPairRelevanceComparator() {
            mCachedDistances = new HashMap<>();
        }

        public double getDistance(UrlDevice urlDevice) {
            if (mCachedDistances.containsKey(urlDevice.getId())) {
                return mCachedDistances.get(urlDevice.getId());
            }
            double distance = Utils.getDistance(urlDevice);
            mCachedDistances.put(urlDevice.getId(), distance);
            return distance;
        }

        @Override
        public int compare(PwPair lhs, PwPair rhs) {
            String lSite = lhs.getPwsResult().getSiteUrl();
            String rSite = rhs.getPwsResult().getSiteUrl();
            if (mFavorites.contains(lSite) == mFavorites.contains(rSite)) {
                return Double.compare(getDistance(lhs.getUrlDevice()), getDistance(rhs.getUrlDevice()));
            } else {
                if (mFavorites.contains(lSite)) {
                    return -1;
                }
                return 1;
            }
        }
    }

    /**
     * Surface a notification to the user that the Physical Web is broadcasting. The notification
     * specifies the transport or URL that is being broadcast and cannot be swiped away.
     * @param context
     * @param stopServiceReceiver
     * @param broadcastNotificationId
     * @param title
     * @param text
     * @param filter
     */
    public static void createBroadcastNotification(Context context, BroadcastReceiver stopServiceReceiver,
            int broadcastNotificationId, CharSequence title, CharSequence text, String filter) {
        Intent resultIntent = new Intent();
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
        stackBuilder.addParentStack(MainActivity.class);
        stackBuilder.addNextIntent(resultIntent);
        PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
        context.registerReceiver(stopServiceReceiver, new IntentFilter(filter));
        PendingIntent pIntent = PendingIntent.getBroadcast(context, 0, new Intent(filter),
                PendingIntent.FLAG_UPDATE_CURRENT);
        NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
                .setSmallIcon(R.drawable.ic_leak_add_white_24dp).setContentTitle(title).setContentText(text)
                .setOngoing(true).addAction(android.R.drawable.ic_menu_close_clear_cancel,
                        context.getString(R.string.stop), pIntent);
        builder.setContentIntent(resultPendingIntent);

        NotificationManager notificationManager = (NotificationManager) context
                .getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify(broadcastNotificationId, builder.build());
    }

    /**
     * Reads the data from the inputStream.
     * @param inputStream The input to read from.
     * @return The entire input stream as a byte array
     * @throws IOException
     */
    public static byte[] getBytes(InputStream inputStream) throws IOException {
        ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();
        int bufferSize = 1024;
        byte[] buffer = new byte[bufferSize];

        int len;
        while ((len = inputStream.read(buffer)) != -1) {
            byteBuffer.write(buffer, 0, len);
        }
        return byteBuffer.toByteArray();
    }

    /**
     * Get set of Blocked hosts.
     */
    public static Set<String> getBlockedHosts() {
        return mBlockedUrls;
    }

    /**
     * Hides all items in the given menu.
     * @param menu The menu to hide items for.
     */
    public static void hideAllMenuItems(Menu menu) {
        menu.findItem(R.id.action_about).setVisible(false);
        menu.findItem(R.id.action_settings).setVisible(false);
        menu.findItem(R.id.block_settings).setVisible(false);
        menu.findItem(R.id.action_demos).setVisible(false);
    }

    /**
     * Check if URL has been favorited.
     * @param siteUrl
     */
    public static boolean isFavorite(String siteUrl) {
        return mFavoriteUrls.contains(siteUrl);
    }

    /**
     * Toggles favorite status.
     * @param siteUrl
     */
    public static void toggleFavorite(String siteUrl) {
        if (isFavorite(siteUrl)) {
            mFavoriteUrls.remove(siteUrl);
            return;
        }
        mFavoriteUrls.add(siteUrl);
    }

    /**
     * Save favorites to shared preferences.
     * @param context To get shared preferences.
     */
    public static void saveFavorites(Context context) {
        // Write the PW Collection
        PreferenceManager.getDefaultSharedPreferences(context).edit().putStringSet(FAVORITES_KEY, mFavoriteUrls)
                .apply();
    }

    /**
     * Get favorites from shared preferences.
     * @param context To get shared preferences.
     */
    public static void restoreFavorites(Context context) {
        mFavoriteUrls = new HashSet<>(PreferenceManager.getDefaultSharedPreferences(context)
                .getStringSet(FAVORITES_KEY, new HashSet<String>()));
    }

    /**
     * Check if any PwPair in the list is favorited.
     * @param pairs List of pwPairs
     */
    public static boolean containsFavorite(List<PwPair> pairs) {
        for (PwPair pair : pairs) {
            if (isFavorite(pair.getPwsResult().getSiteUrl())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check if domain of URL has been blocked.
     * @param siteUrl
     * @return is siteURL blocked
     */
    public static boolean isBlocked(PwPair pwPair) {
        if (Utils.isWifiDirectDevice(pwPair.getUrlDevice())) {
            return mBlockedUrls.contains(Utils.getWifiAddress(pwPair.getUrlDevice()));
        }
        try {
            return mBlockedUrls.contains(new URI(pwPair.getPwsResult().getSiteUrl()).getHost());
        } catch (URISyntaxException e) {
            return false;
        }
    }

    /**
     * Block the host of siteUrl.
     * @param siteUrl
     */
    public static void addBlocked(PwPair pwPair) {
        if (Utils.isWifiDirectDevice(pwPair.getUrlDevice())) {
            mBlockedUrls.add(Utils.getWifiAddress(pwPair.getUrlDevice()));
            return;
        }
        try {
            mBlockedUrls.add(new URI(pwPair.getPwsResult().getSiteUrl()).getHost());
        } catch (URISyntaxException e) {
            return;
        }
    }

    /**
     * Unblock the host.
     * @param host
     */
    public static void removeBlocked(String host) {
        if (mBlockedUrls.contains(host)) {
            mBlockedUrls.remove(host);
        }
    }

    /**
     * Save blocked set to shared preferences.
     * @param context to access shared preferences
     */
    public static void saveBlocked(Context context) {
        // Write the PW Collection
        PreferenceManager.getDefaultSharedPreferences(context).edit().putStringSet("blocked", mBlockedUrls).apply();
    }

    /**
     * Restore blocked set from shared preferences.
     * @param context to access shared preferences
     */
    public static void restoreBlocked(Context context) {
        mBlockedUrls = new HashSet<>(PreferenceManager.getDefaultSharedPreferences(context).getStringSet("blocked",
                new HashSet<String>()));
    }

    public static class WifiDirectInfo {
        public String title;
        public int port;

        public WifiDirectInfo(String title, int port) {
            this.title = title;
            this.port = port;
        }
    }

    /**
     * Checks to see if name matches defined wifi direct structure.
     * @param name WifiDirect device name
     */
    public static WifiDirectInfo parseWifiDirectName(String name) {
        String split[] = name.split("-");
        if (split.length < 3 || !split[0].equals("PW") || !split[split.length - 1].matches("\\d+")) {
            return null;
        }
        return new WifiDirectInfo(name.substring(3, name.lastIndexOf('-')),
                Integer.parseInt(split[split.length - 1]));

    }

    private static class PwsEndpoint {
        public String url;
        public int apiVersion;
        public String apiKey;
        public boolean neededApiKey;

        public PwsEndpoint(String url, int apiVersion, String apiKey) {
            this.url = url;
            this.apiVersion = apiVersion;
            this.apiKey = apiKey;
            this.neededApiKey = needApiKey();
            if (this.neededApiKey) {
                this.url = PROD_ENDPOINT;
                this.apiVersion = PROD_ENDPOINT_VERSION;
            }
        }

        private boolean needApiKey() {
            return apiVersion >= 2 && apiKey.isEmpty();
        }

    }

    private static void throwEncodeException(JSONException e) {
        throw new RuntimeException("Could not encode JSON", e);
    }

    private static void deletePreference(Context context, String preferenceName) {
        context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE).edit().clear().apply();
    }

    private static int getGoogleApiKeyResourceId(Context context) {
        return context.getResources().getIdentifier("google_api_key", "string", context.getPackageName());
    }

    private static PwsEndpoint getCurrentPwsEndpoint(Context context) {
        return new PwsEndpoint(getCurrentPwsEndpointUrl(context), getCurrentPwsEndpointVersion(context),
                getCurrentPwsEndpointApiKey(context));
    }

    private static String getCurrentPwsEndpointString(Context context) {
        String defaultEndpoint = getDefaultPwsEndpointPreferenceString(context);
        return PreferenceManager.getDefaultSharedPreferences(context)
                .getString(context.getString(R.string.pws_endpoint_setting_key), defaultEndpoint);
    }

    private static String getCurrentPwsEndpointUrl(Context context) {
        String endpoint = getCurrentPwsEndpointString(context);
        return endpoint.split(SEPARATOR)[0];
    }

    private static int getCurrentPwsEndpointVersion(Context context) {
        String endpoint = getCurrentPwsEndpointString(context);
        return Integer.parseInt(endpoint.split(SEPARATOR)[1]);
    }

    private static String getCurrentPwsEndpointApiKey(Context context) {
        String endpoint = getCurrentPwsEndpointString(context);
        if (endpoint.endsWith(SEPARATOR) || endpoint.endsWith("null")) {
            return "";
        }
        return endpoint.split(SEPARATOR)[2];
    }

    private static String readCustomPwsEndpointUrl(Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context)
                .getString(context.getString(R.string.custom_pws_url_key), "");
    }

    private static int readCustomPwsEndpointVersion(Context context) {
        return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString(
                context.getString(R.string.custom_pws_version_key),
                context.getString(R.string.custom_pws_version_default)));
    }

    private static String readCustomPwsEndpointApiKey(Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context)
                .getString(context.getString(R.string.custom_pws_api_key_key), "");
    }

    /**
     * Gets whether the user has optedIn from SharedPreferences.
     * @param context The context for the SharedPreferences.
     * @return True if the user has optedIn otherwise false.
     */
    public static boolean checkIfUserHasOptedIn(Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(USER_OPTED_IN_KEY, false);
    }

    public abstract static class UrlDeviceDiscoveryServiceConnection implements ServiceConnection {
        private Context mContext;

        @Override
        public void onServiceConnected(ComponentName className, IBinder service) {
            // Forward the service to the implementing class
            serviceHandler((UrlDeviceDiscoveryService.LocalBinder) service);
            mContext.unbindService(this);
        }

        @Override
        public void onServiceDisconnected(ComponentName className) {
        }

        public void connect(Context context) {
            mContext = context;
            Intent intent = new Intent(mContext, UrlDeviceDiscoveryService.class);
            mContext.startService(intent);
            mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
        }

        public abstract void serviceHandler(UrlDeviceDiscoveryService.LocalBinder localBinder);
    }

    /**
     * Delete the cached results from the UrlDeviceDisoveryService.
     * @param context The context for the service.
     */
    public static void deleteCache(Context context) {
        new UrlDeviceDiscoveryServiceConnection() {
            @Override
            public void serviceHandler(UrlDeviceDiscoveryService.LocalBinder localBinder) {
                localBinder.getServiceInstance().clearCache();
            }
        }.connect(context);
    }

    /**
     * Starts scanning with UrlDeviceDisoveryService.
     * @param context The context for the service.
     */
    public static void startScan(Context context) {
        new UrlDeviceDiscoveryServiceConnection() {
            @Override
            public void serviceHandler(UrlDeviceDiscoveryService.LocalBinder localBinder) {
                localBinder.getServiceInstance().restartScan();
            }
        }.connect(context);
    }

    /**
     * Format the endpoint URL, version, and API key.
     * @param pwsUrl The URL of the Physical Web Service.
     * @param pwsVersion The API version the PWS is running.
     * @param apiKey The API key for the PWS.
     * @return The PWS endpoint formatted for saving to SharedPreferences.
     */
    public static String formatEndpointForSharedPrefernces(String pwsUrl, int pwsVersion, String apiKey) {
        pwsUrl = pwsUrl == null ? "" : pwsUrl;
        apiKey = apiKey == null ? "" : apiKey;
        return pwsUrl + SEPARATOR + pwsVersion + SEPARATOR + apiKey;
    }

    /**
     * Get the default PWS Endpoint formatted for SharedPreferences.
     * @param context The context for the SharedPreferences.
     * @return The default PWS endpoint formatted for saving to SharedPreferences.
     */
    public static String getDefaultPwsEndpointPreferenceString(Context context) {
        if (isGoogleApiKeyAvailable(context)) {
            return formatEndpointForSharedPrefernces(GOOGLE_ENDPOINT, GOOGLE_ENDPOINT_VERSION,
                    getGoogleApiKey(context));
        }
        return formatEndpointForSharedPrefernces(PROD_ENDPOINT, PROD_ENDPOINT_VERSION, "");
    }

    /**
     * Saves the optIn preference in the default shared preferences file.
     * @param context The context for the SharedPreferences.
     */
    public static void setOptInPreference(Context context) {
        PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(USER_OPTED_IN_KEY, true).apply();
    }

    /**
     * Saves the endpoint to SharedPreferences.
     * @param context The context for the SharedPreferences.
     * @param endpoint The endpoint formatted for SharedPreferences.
     */
    public static void setPwsEndpointPreference(Context context, String endpoint) {
        PreferenceManager.getDefaultSharedPreferences(context).edit()
                .putString(context.getString(R.string.pws_endpoint_setting_key), endpoint).apply();
    }

    /**
     * Saves the default settings to the SharedPreferences.
     * @param context The context for the SharedPreferences.
     */
    public static void setSharedPreferencesDefaultValues(Context context) {
        PreferenceManager.setDefaultValues(context, R.xml.settings, false);
        setPwsEndpointPreference(context, getCurrentPwsEndpointString(context));
        if (context.getSharedPreferences(MAIN_PREFS_KEY, Context.MODE_PRIVATE).getBoolean(USER_OPTED_IN_KEY,
                false)) {
            setOptInPreference(context);
            deletePreference(context, MAIN_PREFS_KEY);
            deletePreference(context, DISCOVERY_SERVICE_PREFS_KEY);
        }
    }

    /**
     * Sets the client's endpoint to the currently selected endpoint.
     * @param context The context for the SharedPreferences.
     * @param pwsClient The client used for requests to the Physical Web Service.
     * @return If the endpoint was successfully set to current endpoint or if it had to be reverted.
     *         to the default endpoint.
     */
    public static boolean setPwsEndpoint(Context context, PwsClient pwsClient) {
        PwsEndpoint endpoint = getCurrentPwsEndpoint(context);
        pwsClient.setEndpoint(endpoint.url, endpoint.apiVersion, endpoint.apiKey);
        return !endpoint.neededApiKey;
    }

    /**
     * Sets the client's endpoint to the currently selected endpoint.
     * @param context The context for the SharedPreferences.
     * @param physicalWebCollection The client used for requests to the Physical Web Service.
     * @return If the endpoint was successfully set to current endpoint or if it had to be reverted.
     *         to the default endpoint.
     */
    public static boolean setPwsEndpoint(Context context, PhysicalWebCollection physicalWebCollection) {
        PwsEndpoint endpoint = getCurrentPwsEndpoint(context);
        physicalWebCollection.setPwsEndpoint(endpoint.url, endpoint.apiVersion, endpoint.apiKey);
        return !endpoint.neededApiKey;
    }

    /**
     * Sets the client's endpoint to the Google endpoint.
     * @param context The context for the SharedPreferences.
     * @param pwsClient The client used for requests to the Physical Web Service.
     */
    public static void setPwsEndPointToGoogle(Context context, PwsClient pwsClient) {
        pwsClient.setEndpoint(GOOGLE_ENDPOINT, GOOGLE_ENDPOINT_VERSION, getGoogleApiKey(context));
    }

    /**
     * Checks if the currently selected PWS has a valid format, not that it exists.
     * @param context The context for the SharedPreferences.
     * @return If the current PWS configured properly.
     */
    public static boolean isCurrentPwsSelectionValid(Context context) {
        String endpoint = getCurrentPwsEndpointUrl(context);
        int apiVersion = getCurrentPwsEndpointVersion(context);
        String apiKey = getCurrentPwsEndpointApiKey(context);
        return !endpoint.isEmpty() && !(apiVersion >= 2 && apiKey.isEmpty());
    }

    /**
     * Checks if the Google API key resource exists.
     * @param context The context for the resources.
     * @return If the Google API key is available.
     */
    public static boolean isGoogleApiKeyAvailable(Context context) {
        return getGoogleApiKeyResourceId(context) != 0;
    }

    /**
     * Get the Google API key if available.
     * @param context The context for the resources.
     * @return The API key for the Google PWS if available or an empty string.
     */
    public static String getGoogleApiKey(Context context) {
        int resourceId = getGoogleApiKeyResourceId(context);
        return resourceId != 0 ? context.getString(resourceId) : "";
    }

    /**
     * Gets the saved settings for the custom PWS.
     * @param context The context for the SharedPreferences.
     * @return The custom PWS endpoint formatted for SharedPrefences.
     */
    public static String getCustomPwsEndpoint(Context context) {
        return formatEndpointForSharedPrefernces(readCustomPwsEndpointUrl(context),
                readCustomPwsEndpointVersion(context), readCustomPwsEndpointApiKey(context));
    }

    /**
     * Get the saved setting for enabling Fatbeacon.
     * @param context The context for the SharedPreferences.
     * @return The enable Fatbeacon setting.
     */
    public static boolean isFatBeaconEnabled(Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context)
                .getBoolean(context.getString(R.string.fatbeacon_key), false);
    }

    /**
     * Get the saved setting for enabling mDNS folder.
     * @param context The context for the SharedPreferences.
     * @return The enable mDNS folder setting.
     */
    public static boolean isMdnsEnabled(Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context)
                .getBoolean(context.getString(R.string.mDNS_key), false);
    }

    /**
     * Get the saved setting for enabling Wifi direct.
     * @param context The context for the SharedPreferences.
     * @return The enable wifi direct setting.
     */
    public static boolean isWifiDirectEnabled(Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context)
                .getBoolean(context.getString(R.string.wifi_direct_key), false);
    }

    /**
     * Get the saved setting for debug View.
     * @param context The context for the SharedPreferences.
     * @return The debug view setting.
     */
    public static boolean isDebugViewEnabled(Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context)
                .getBoolean(context.getString(R.string.debug_key), false);
    }

    /**
     * Get the saved setting for enabling mDNS folder.
     * @param context The context for the SharedPreferences.
     * @return The enable mDNS folder setting.
     */
    public static int getWifiDirectPort(Context context) {
        String port = PreferenceManager.getDefaultSharedPreferences(context)
                .getString(context.getString(R.string.wifi_port_key), "1234");
        if (port.matches("\\d+")) {
            return Integer.parseInt(port);
        }
        return 1234;
    }

    /**
     * Toast the user to indicate the API key is missing.
     * @param context The context for the resources.
     */
    public static void warnUserOnMissingApiKey(Context context) {
        Toast.makeText(context, R.string.error_api_key_no_longer_available, Toast.LENGTH_SHORT).show();
    }

    /**
     * Create an intent for opening a URL.
     * @param pwsResult The result that has the URL the user clicked on.
     * @return The intent that opens the URL.
     */
    public static Intent createNavigateToUrlIntent(PwsResult pwsResult) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setData(Uri.parse(pwsResult.getSiteUrl()));
        return intent;
    }

    /**
     * Setup an intent to open the URL.
     * @param pwsResult The result that has the URL the user clicked on.
     * @param context The context for the activity.
     * @return The intent that opens the URL.
     */
    public static PendingIntent createNavigateToUrlPendingIntent(PwsResult pwsResult, Context context) {
        Intent intent = createNavigateToUrlIntent(pwsResult);
        int requestID = (int) System.currentTimeMillis();
        return PendingIntent.getActivity(context, requestID, intent, 0);
    }

    /**
     * Gets the top ranked PwPair with the given groupId.
     * @param pwCollection The collection of PwPairs.
     * @param groupId The group id of the requested PwPair.
     * @return The top ranked PwPair with a given group id if it exist.
     */
    public static PwPair getTopRankedPwPairByGroupId(PhysicalWebCollection pwCollection, String groupId) {
        // This does the same thing as the PhysicalWebCollection method, only it uses our custom
        // getGroupId method.
        for (PwPair pwPair : pwCollection.getGroupedPwPairsSortedByRank(new PwPairRelevanceComparator())) {
            if (getGroupId(pwPair.getPwsResult()).equals(groupId)) {
                return pwPair;
            }
        }
        return null;
    }

    /**
     * Decode the downloaded icon to a Bitmap.
     * @param pwCollection The collection where the icon is stored.
     * @param pwsResult The result the icon is for.
     * @return The icon as a Bitmap.
     */
    public static Bitmap getBitmapIcon(PhysicalWebCollection pwCollection, PwsResult pwsResult) {
        byte[] iconBytes = pwCollection.getIcon(pwsResult.getIconUrl());
        if (iconBytes == null) {
            return null;
        }
        return BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.length);
    }

    /**
     * Get the scan time for a device if available.
     * @param urlDevice The device to get the scan time for.
     * @return The scan time in millisecond for the device.
     * @throws RuntimeException If the device doesn't have a scan time.
     */
    public static long getScanTimeMillis(UrlDevice urlDevice) {
        try {
            return urlDevice.getExtraLong(SCANTIME_KEY);
        } catch (JSONException e) {
            throw new RuntimeException("Scan time not available in device " + urlDevice.getId(), e);
        }
    }

    /**
     * Checks if device is public.
     * @param urlDevice The device that is getting checked.
     * @return If the device is public or not.
     */
    public static boolean isPublic(UrlDevice urlDevice) {
        return urlDevice.optExtraBoolean(PUBLIC_KEY, true);
    }

    /**
     * Checks if device is PWS resolvable.
     * @param urlDevice The device that is getting checked.
     * @return If the device is resolvable or not.
     */
    public static boolean isResolvableDevice(UrlDevice urlDevice) {
        String type = urlDevice.optExtraString(TYPE_KEY, "");
        return type.equals(BLE_DEVICE_TYPE) || type.equals(SSDP_DEVICE_TYPE)
                || type.equals(MDNS_PUBLIC_DEVICE_TYPE);
    }

    /**
     * Checks if device is Bluetooth Low Energy.
     * @param urlDevice The device that is getting checked.
     * @return If the device is BLE or not.
     */
    public static boolean isBleUrlDevice(UrlDevice urlDevice) {
        return urlDevice.optExtraString(TYPE_KEY, "").equals(BLE_DEVICE_TYPE);
    }

    /**
     * Gets if the device is a FatBeacon.
     * @param urlDevice The device that is getting checked.
     * @return Is a FatBeacon device
     */
    public static boolean isFatBeaconDevice(UrlDevice urlDevice) {
        return urlDevice.optExtraString(TYPE_KEY, "").equals(FAT_BEACON_DEVICE_TYPE);
    }

    /**
     * Checks if device is mdns device broadcasting public url.
     * @param urlDevice The device that is getting checked.
     * @return If the device is mdns public or not.
     */
    public static boolean isMDNSPublicDevice(UrlDevice urlDevice) {
        return urlDevice.optExtraString(TYPE_KEY, "").equals(MDNS_PUBLIC_DEVICE_TYPE);
    }

    /**
     * Checks if device is Local.
     * @param urlDevice The device that is getting checked.
     * @return If the device is local or not.
     */
    public static boolean isMDNSLocalDevice(UrlDevice urlDevice) {
        return urlDevice.optExtraString(TYPE_KEY, "").equals(MDNS_LOCAL_DEVICE_TYPE);
    }

    /**
     * Gets if the device is Wifi Direct.
     * @param urlDevice The device that is getting checked.
     * @return Is a WifiDirect device
     */
    public static boolean isWifiDirectDevice(UrlDevice urlDevice) {
        return urlDevice.optExtraString(TYPE_KEY, "").equals(WIFI_DIRECT_DEVICE_TYPE);
    }

    /**
     * Gets if the device is SSDP.
     * @param urlDevice The device that is getting checked.
     * @return Is a SSDP device
     */
    public static boolean isSSDPDevice(UrlDevice urlDevice) {
        return urlDevice.optExtraString(TYPE_KEY, "").equals(SSDP_DEVICE_TYPE);
    }

    /**
     * Gets the device RSSI if it is Bluetooth Low Energy.
     * @param urlDevice The device that is getting checked.
     * @return The RSSI for the device.
     * @throws RuntimeException If the device is not BLE.
     */
    public static int getRssi(UrlDevice urlDevice) {
        try {
            return urlDevice.getExtraInt(RSSI_KEY);
        } catch (JSONException e) {
            throw new RuntimeException("Tried to get RSSI from non-ble device " + urlDevice.getId(), e);
        }
    }

    /**
     * Gets the device TX power if it is Bluetooth Low Energy.
     * @param urlDevice The device that is getting checked.
     * @return The TX power for the device.
     * @throws RuntimeException If the device is not BLE.
     */
    public static int getTxPower(UrlDevice urlDevice) {
        try {
            return urlDevice.getExtraInt(TXPOWER_KEY);
        } catch (JSONException e) {
            throw new RuntimeException("Tried to get TX power from non-ble device " + urlDevice.getId(), e);
        }
    }

    /**
     * Gets the UrlDevice Title.
     * @param urlDevice The device that is getting checked.
     * @return The Title for the device.
     * @throws RuntimeException if no title present.
     */
    public static String getTitle(UrlDevice urlDevice) {
        try {
            return urlDevice.getExtraString(TITLE_KEY);
        } catch (JSONException e) {
            throw new RuntimeException("Tried to get Title when no title set " + urlDevice.getId(), e);
        }
    }

    /**
     * Gets the UrlDevice Description.
     * @param urlDevice The device that is getting checked.
     * @return The Description for the device.
     * @throws RuntimeException if no description present.
     */
    public static String getDescription(UrlDevice urlDevice) {
        try {
            return urlDevice.getExtraString(DESCRIPTION_KEY);
        } catch (JSONException e) {
            throw new RuntimeException("Tried to get Description when no description set " + urlDevice.getId(), e);
        }
    }

    /**
     * Gets the UrlDevice MAC address.
     * @param urlDevice The device that is getting checked.
     * @return The MAC address for the device.
     * @throws RuntimeException if no address present.
     */
    public static String getWifiAddress(UrlDevice urlDevice) {
        try {
            return urlDevice.getExtraString(WIFIDIRECT_KEY);
        } catch (JSONException e) {
            throw new RuntimeException("Tried to get address when no description set " + urlDevice.getId(), e);
        }
    }

    /**
     * Gets the UrlDevice port.
     * @param urlDevice The device that is getting checked.
     * @return The port for the device.
     * @throws RuntimeException if no port present.
     */
    public static int getWifiPort(UrlDevice urlDevice) {
        try {
            return urlDevice.getExtraInt(WIFIDIRECT_PORT_KEY);
        } catch (JSONException e) {
            throw new RuntimeException("Tried to get port when no port set " + urlDevice.getId(), e);
        }
    }

    /**
     * Gets the amount of time in milliseconds to get the result from the PWS if available.
     * @param pwsResult The result that is being queried.
     * @return The trip time for the result.
     * @throws RuntimeException If the trip time is not recorded.
     */
    public static long getPwsTripTimeMillis(PwsResult pwsResult) {
        try {
            return pwsResult.getExtraLong(PWSTRIPTIME_KEY);
        } catch (JSONException e) {
            throw new RuntimeException("PWS trip time not recorded in PwsResult");
        }
    }

    /**
     * Gets the groupId for the result.
     * @param pwsResult The result that is being queried.
     * @return The groupId for the result.
     */
    public static String getGroupId(PwsResult pwsResult) {
        // The PWS does not always give us a group id yet.
        if (pwsResult.getGroupId() == null || pwsResult.getGroupId().equals("")) {
            try {
                return new URI(pwsResult.getSiteUrl()).getHost() + pwsResult.getTitle();
            } catch (URISyntaxException e) {
                return pwsResult.getSiteUrl();
            }
        }
        return pwsResult.getGroupId();
    }

    /**
     * Updates the region resolver with the device.
     * @param urlDevice The device to update region with.
     */
    public static void updateRegion(UrlDevice urlDevice) {
        REGION_RESOLVER.onUpdate(urlDevice.getId(), getRssi(urlDevice), getTxPower(urlDevice));
    }

    /**
     * Gets the smoothed RSSI for device from the region resolver.
     * @param urlDevice The device being queried.
     * @return The smoothed RSSI for the device.
     */
    public static double getSmoothedRssi(UrlDevice urlDevice) {
        return REGION_RESOLVER.getSmoothedRssi(urlDevice.getId());
    }

    /**
     * Gets the distance for device from the region resolver.
     * @param urlDevice The device being queried.
     * @return The distance for the device.
     */
    public static double getDistance(UrlDevice urlDevice) {
        return REGION_RESOLVER.getDistance(urlDevice.getId());
    }

    /**
     * Gets the region string for device from the region resolver.
     * @param urlDevice The device being queried.
     * @return The region string for the device.
     */
    public static String getRegionString(UrlDevice urlDevice) {
        return RangingUtils.toString(REGION_RESOLVER.getRegion(urlDevice.getId()));
    }

    static class UrlDeviceBuilder extends UrlDevice.Builder {

        /**
         * Constructor for the UrlDeviceBuilder.
         * @param id The id of the UrlDevice.
         * @param url The url of the UrlDevice.
         */
        public UrlDeviceBuilder(String id, String url) {
            super(id, url);
        }

        /**
        * Set the device type.
        * @return The builder with type set.
        */
        public UrlDeviceBuilder setDeviceType(String type) {
            addExtra(TYPE_KEY, type);
            return this;
        }

        /**
         * Setter for the ScanTimeMillis.
         * @param timeMillis The scan time of the UrlDevice.
         * @return The builder with ScanTimeMillis set.
         */
        public UrlDeviceBuilder setScanTimeMillis(long timeMillis) {
            addExtra(SCANTIME_KEY, timeMillis);
            return this;
        }

        /**
         * Set the public key to false.
         * @return The builder with public set to false.
         */
        public UrlDeviceBuilder setPrivate() {
            addExtra(PUBLIC_KEY, false);
            return this;
        }

        /**
         * Set the public key to true.
         * @return The builder with public set to true.
         */
        public UrlDeviceBuilder setPublic() {
            addExtra(PUBLIC_KEY, true);
            return this;
        }

        /**
         * Set the title.
         * @param title corresonding to UrlDevice.
         * @return The builder with title
         */
        public UrlDeviceBuilder setTitle(String title) {
            addExtra(TITLE_KEY, title);
            return this;
        }

        /**
         * Set the description.
         * @param description corresonding to UrlDevice.
         * @return The builder with description
         */
        public UrlDeviceBuilder setDescription(String description) {
            addExtra(DESCRIPTION_KEY, description);
            return this;
        }

        /**
         * Set wifi-direct MAC address.
         * @param MAC address corresonding to UrlDevice.
         * @return The builder with address
         */
        public UrlDeviceBuilder setWifiAddress(String address) {
            addExtra(WIFIDIRECT_KEY, address);
            return this;
        }

        /**
        * Set wifi-direct port.
        * @param port corresonding to UrlDevice.
        * @return The builder with port
        */
        public UrlDeviceBuilder setWifiPort(int port) {
            addExtra(WIFIDIRECT_PORT_KEY, port);
            return this;
        }

        /**
         * Setter for the RSSI.
         * @param rssi The RSSI of the UrlDevice.
         * @return The builder with RSSI set.
         */
        public UrlDeviceBuilder setRssi(int rssi) {
            addExtra(RSSI_KEY, rssi);
            return this;
        }

        /**
         * Setter for the TX power.
         * @param txPower The TX power of the UrlDevice.
         * @return The builder with TX power set.
         */
        public UrlDeviceBuilder setTxPower(int txPower) {
            addExtra(TXPOWER_KEY, txPower);
            return this;
        }
    }

    static class PwsResultBuilder extends PwsResult.Builder {
        /**
         * Constructor for the PwsResultBuilder.
         * @param pwsResult The base result of the PwsResultBuilder.
         */
        public PwsResultBuilder(PwsResult pwsResult) {
            super(pwsResult);
        }

        /**
         * Setter for the PWS Trip Time.
         * @param pwsResult The pwsResult.
         * @param timeMillis The PWS Trip Time for the result.
         * @return The builder PWS Trip Time set.
         */
        public PwsResultBuilder setPwsTripTimeMillis(PwsResult pwsResult, long timeMillis) {
            addExtra(PWSTRIPTIME_KEY, timeMillis);
            return this;
        }
    }

    /**
     * Determines if a file is gzipped by examining the signature of
     * the file, which is the first two bytes.
     * @param file to be determined if is gzipped.
     * @return true If the contents of the file are gzipped otherwise false.
     */
    public static boolean isGzippedFile(File file) {
        InputStream input;

        try {
            input = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            return false;
        }

        byte[] signature = new byte[GZIP_SIGNATURE_LENGTH];

        try {
            input.read(signature);
        } catch (IOException e) {
            return false;
        }

        return ((signature[0] == (byte) (GZIPInputStream.GZIP_MAGIC))
                && (signature[1] == (byte) (GZIPInputStream.GZIP_MAGIC >> 8)));
    }

    /**
     * Out-of-place Gunzips from src to dest.
     * @param src file containing gzipped information.
     * @param dest file to place decompressed information.
     * @return File that has decompressed information.
     */
    public static File gunzip(File src, File dest) {

        byte[] buffer = new byte[1024];

        try {

            GZIPInputStream gzis = new GZIPInputStream(new FileInputStream(src));

            FileOutputStream out = new FileOutputStream(dest);

            int len;
            while ((len = gzis.read(buffer)) > 0) {
                out.write(buffer, 0, len);
            }

            gzis.close();
            out.close();

        } catch (IOException ex) {
            ex.printStackTrace();
        }
        return dest;
    }
}