/** * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. * * You are hereby granted a non-exclusive, worldwide, royalty-free license to use, * copy, modify, and distribute this software in source code or binary form for use * in connection with the web services and APIs provided by Facebook. * * As with any software that integrates with the Facebook platform, your use of * this software is subject to the Facebook Developer Principles and Policies * [http://developers.facebook.com/policy/]. This copyright notice shall be * included in all copies or substantial portions of the software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.facebook.places; import android.location.Location; import android.os.Bundle; import android.support.annotation.Nullable; import android.text.TextUtils; import com.facebook.AccessToken; import com.facebook.FacebookException; import com.facebook.GraphRequest; import com.facebook.HttpMethod; import com.facebook.internal.Utility; import com.facebook.places.internal.LocationPackageManager; import com.facebook.places.internal.LocationPackageRequestParams; import com.facebook.places.internal.ScannerException; import com.facebook.places.internal.BluetoothScanResult; import com.facebook.places.model.CurrentPlaceFeedbackRequestParams; import com.facebook.places.internal.LocationPackage; import com.facebook.places.model.PlaceInfoRequestParams; import com.facebook.places.model.PlaceSearchRequestParams; import com.facebook.places.model.CurrentPlaceRequestParams; import com.facebook.places.model.CurrentPlaceRequestParams.ConfidenceLevel; import com.facebook.places.internal.WifiScanResult; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.List; import java.util.Locale; import java.util.Set; /** * Provides an interface to search and query the Places Graph. * Supports querying the end user's' current place, searching nearby places, and fetching * place information details. */ public class PlaceManager { private static final String SEARCH = "search"; private static final String CURRENT_PLACE_RESULTS = "current_place/results"; private static final String CURRENT_PLACE_FEEDBACK = "current_place/feedback"; private static final String PARAM_ACCESS_POINTS = "access_points"; private static final String PARAM_ACCURACY = "accuracy"; private static final String PARAM_ALTITUDE = "altitude"; private static final String PARAM_BLUETOOTH = "bluetooth"; private static final String PARAM_CATEGORIES = "categories"; private static final String PARAM_CENTER = "center"; private static final String PARAM_COORDINATES = "coordinates"; private static final String PARAM_CURRENT_CONNECTION = "current_connection"; private static final String PARAM_DISTANCE = "distance"; private static final String PARAM_ENABLED = "enabled"; private static final String PARAM_FIELDS = "fields"; private static final String PARAM_FREQUENCY = "frequency"; private static final String PARAM_HEADING = "heading"; private static final String PARAM_LATITUDE = "latitude"; private static final String PARAM_LIMIT = "limit"; private static final String PARAM_LONGITUDE = "longitude"; private static final String PARAM_MAC_ADDRESS = "mac_address"; private static final String PARAM_MIN_CONFIDENCE_LEVEL = "min_confidence_level"; private static final String PARAM_PAYLOAD = "payload"; private static final String PARAM_PLACE_ID = "place_id"; private static final String PARAM_Q = "q"; private static final String PARAM_RSSI = "rssi"; private static final String PARAM_SCANS = "scans"; private static final String PARAM_SIGNAL_STRENGTH = "signal_strength"; private static final String PARAM_SPEED = "speed"; private static final String PARAM_SSID = "ssid"; private static final String PARAM_SUMMARY = "summary"; private static final String PARAM_TRACKING = "tracking"; private static final String PARAM_TYPE = "type"; private static final String PARAM_WAS_HERE = "was_here"; private static final String PARAM_WIFI = "wifi"; /** * Describes an error that occurred while retrieving the current location. */ public enum LocationError { /** * The location permissions are denied. The SDK requires permissions * {@code "android.permission.ACCESS_FINE_LOCATION"} or * {@code "android.permission.ACCESS_COARSE_LOCATION"} to retrieve the current location. */ LOCATION_PERMISSION_DENIED, /** * The location could not be retrieved because location services are not enabled. */ LOCATION_SERVICES_DISABLED, /** * The location could be retrieved within the allotted amount of time. */ LOCATION_TIMEOUT, /** * An unknown error occurred. */ UNKNOWN_ERROR, } /** * Callback invoked when a request has been constructed and is ready to be executed. * To be used with {@link PlaceManager#newCurrentPlaceRequest(CurrentPlaceRequestParams, * OnRequestReadyCallback)} and {@link PlaceManager#newPlaceSearchRequest( * PlaceSearchRequestParams, OnRequestReadyCallback)}. */ public interface OnRequestReadyCallback { /** * Method invoked when the request can't be generated due to an error retrieving the current * device location. * @param error the error description */ void onLocationError(LocationError error); /** * Method invoked when the provided {@code GraphRequest} is ready to be executed. * Set a callback on it to handle the response using {@code setCallback}, and then * execute the request. * * @param graphRequest the request that's ready to be executed. */ void onRequestReady(GraphRequest graphRequest); } private PlaceManager() { // No public constructor required as all methods are static } /** * Creates a new place search request centered around the current device location. * The SDK will retrieve the current device location using * {@link android.location.LocationManager} * <p> * With the Places Graph, you can search for millions of places worldwide and retrieve * information like number of checkins, ratings, and addresses all with one request. * <p> * The specified {@link OnRequestReadyCallback} will be invoked once the request has been * generated and is ready to be executed. * * @param requestParams the request parameters. See {@link PlaceSearchRequestParams} * @param callback the {@link OnRequestReadyCallback} invoked when the {@link GraphRequest} * has been generated and is ready to be executed */ public static void newPlaceSearchRequest( final PlaceSearchRequestParams requestParams, final OnRequestReadyCallback callback) { LocationPackageRequestParams.Builder builder = new LocationPackageRequestParams.Builder(); builder.setWifiScanEnabled(false); builder.setBluetoothScanEnabled(false); LocationPackageManager.requestLocationPackage( builder.build(), new LocationPackageManager.Listener() { @Override public void onLocationPackage(LocationPackage locationPackage) { if (locationPackage.locationError == null) { GraphRequest graphRequest = newPlaceSearchRequestForLocation( requestParams, locationPackage.location); callback.onRequestReady(graphRequest); } else { callback.onLocationError(getLocationError(locationPackage.locationError)); } } }); } /** * Creates a new place search request centered around the specified location. * If the location provided is null, the search will be completed globally. * At least a location or a search text must be provided. * <p> * With the Places Graph, you can search for millions of places worldwide and retrieve * information like number of checkins, ratings, and addresses all with one request. * <p> * Returns a new GraphRequest that is configured to perform a place search. * * @param requestParams the request parameters. See {@link PlaceSearchRequestParams} * @param location the {@link Location} around which to search * @return a {@link GraphRequest} that is ready to be executed * @throws FacebookException thrown if neither {@code location} nor {@code searchText} * is specified */ public static GraphRequest newPlaceSearchRequestForLocation( PlaceSearchRequestParams requestParams, Location location) { String searchText = requestParams.getSearchText(); if (location == null && searchText == null) { throw new FacebookException("Either location or searchText must be specified."); } int limit = requestParams.getLimit(); Set<String> fields = requestParams.getFields(); Set<String> categories = requestParams.getCategories(); Bundle parameters = new Bundle(7); parameters.putString(PARAM_TYPE, "place"); if (location != null) { parameters.putString( PARAM_CENTER, String.format( Locale.US, "%f,%f", location.getLatitude(), location.getLongitude())); int distance = requestParams.getDistance(); if (distance > 0) { parameters.putInt(PARAM_DISTANCE, distance); } } if (limit > 0) { parameters.putInt(PARAM_LIMIT, limit); } if (!Utility.isNullOrEmpty(searchText)) { parameters.putString(PARAM_Q, searchText); } if (categories != null && !categories.isEmpty()) { JSONArray array = new JSONArray(); for (String category : categories) { array.put(category); } parameters.putString(PARAM_CATEGORIES, array.toString()); } if (fields != null && !fields.isEmpty()) { parameters.putString(PARAM_FIELDS, TextUtils.join(",", fields)); } return new GraphRequest( AccessToken.getCurrentAccessToken(), SEARCH, parameters, HttpMethod.GET); } /** * Creates a new place info request. * <p> * The Places Graph exposes a rich set of information about places. * If the request is authenticated with a user access token, * you can also obtain social information such as the number of friends who have liked and * checked into the PlaceFields. The specific friends are also available if they have * authenticated the app with the user_tagged_places and user_likes permissions. * <p> * Returns a new {@link GraphRequest} that is configured to perform a place info request. * * @param requestParams the request parameters, a {@link PlaceInfoRequestParams#getPlaceId()} * must be specified. * @return a {@link GraphRequest} that is ready to be executed * @throws FacebookException thrown if a {@link PlaceInfoRequestParams#getPlaceId()} is not * specified. */ public static GraphRequest newPlaceInfoRequest( PlaceInfoRequestParams requestParams) { String placeId = requestParams.getPlaceId(); if (placeId == null) { throw new FacebookException("placeId must be specified."); } Bundle parameters = new Bundle(1); Set<String> fields = requestParams.getFields(); if (fields != null && !fields.isEmpty()) { parameters.putString(PARAM_FIELDS, TextUtils.join(",", fields)); } return new GraphRequest( AccessToken.getCurrentAccessToken(), placeId, parameters, HttpMethod.GET); } /** * Creates a new current place request. * <p> * The current place request estimates the place where the user is currently located. * The response contains a list of places and their associated confidence levels. * <p> * If a location is not specified in {@link CurrentPlaceRequestParams}, then the SDK * retrieves the current location using {@link android.location.LocationManager}. * * @param requestParams the request parameters. See {@link CurrentPlaceRequestParams} * @param callback a {@link OnRequestReadyCallback} that is invoked when the * {@link GraphRequest} has been created and is ready to be executed. */ public static void newCurrentPlaceRequest( final CurrentPlaceRequestParams requestParams, final OnRequestReadyCallback callback) { Location location = requestParams.getLocation(); CurrentPlaceRequestParams.ScanMode scanMode = requestParams.getScanMode(); LocationPackageRequestParams.Builder builder = new LocationPackageRequestParams.Builder(); // Don't scan for a location if one is provided. builder.setLocationScanEnabled(location == null); if (scanMode != null && scanMode == CurrentPlaceRequestParams.ScanMode.LOW_LATENCY) { // In low-latency mode, avoid active Wi-Fi scanning which can takes // several seconds. builder.setWifiActiveScanAllowed(false); } LocationPackageManager.requestLocationPackage( builder.build(), new LocationPackageManager.Listener() { @Override public void onLocationPackage(LocationPackage locationPackage) { if (locationPackage.locationError != null) { callback.onLocationError( getLocationError(locationPackage.locationError)); } else { Bundle parameters = getCurrentPlaceParameters( requestParams, locationPackage); GraphRequest graphRequest = new GraphRequest( AccessToken.getCurrentAccessToken(), CURRENT_PLACE_RESULTS, parameters, HttpMethod.GET); callback.onRequestReady(graphRequest); } } }); } /** * Creates a new current place feedback request. * <p> * This request allows users to provide feedback on the accuracy of the current place * estimate. This information is used to improve the accuracy of our results. * <p> * Returns a new GraphRequest that is configured to perform a current place feedback request. * * @param requestParams the request parameters. See {@link CurrentPlaceFeedbackRequestParams} * @return a {@link GraphRequest} that is ready to be executed * @throws FacebookException thrown if parameters * {@link CurrentPlaceFeedbackRequestParams#getPlaceId()}, * {@link CurrentPlaceFeedbackRequestParams#getTracking()}, or * {@link CurrentPlaceFeedbackRequestParams#wasHere()} are missing */ public static GraphRequest newCurrentPlaceFeedbackRequest( CurrentPlaceFeedbackRequestParams requestParams) { String placeId = requestParams.getPlaceId(); String tracking = requestParams.getTracking(); Boolean wasHere = requestParams.wasHere(); if (tracking == null || placeId == null || wasHere == null) { throw new FacebookException("tracking, placeId and wasHere must be specified."); } Bundle parameters = new Bundle(3); parameters.putString(PARAM_TRACKING, tracking); parameters.putString(PARAM_PLACE_ID, placeId); parameters.putBoolean(PARAM_WAS_HERE, wasHere); return new GraphRequest( AccessToken.getCurrentAccessToken(), CURRENT_PLACE_FEEDBACK, parameters, HttpMethod.POST); } private static Bundle getCurrentPlaceParameters( CurrentPlaceRequestParams request, LocationPackage locationPackage) throws FacebookException { if (request == null) { throw new FacebookException("Request and location must be specified."); } if (locationPackage == null) { locationPackage = new LocationPackage(); } if (locationPackage.location == null) { locationPackage.location = request.getLocation(); } if (locationPackage.location == null) { throw new FacebookException("A location must be specified"); } try { Bundle parameters = new Bundle(6); parameters.putString(PARAM_SUMMARY, PARAM_TRACKING); int limit = request.getLimit(); if (limit > 0) { parameters.putInt(PARAM_LIMIT, limit); } Set<String> fields = request.getFields(); if (fields != null && !fields.isEmpty()) { parameters.putString(PARAM_FIELDS, TextUtils.join(",", fields)); } // Coordinates. Location location = locationPackage.location; JSONObject coordinates = new JSONObject(); coordinates.put(PARAM_LATITUDE, location.getLatitude()); coordinates.put(PARAM_LONGITUDE, location.getLongitude()); if (location.hasAccuracy()) { coordinates.put(PARAM_ACCURACY, location.getAccuracy()); } if (location.hasAltitude()) { coordinates.put(PARAM_ALTITUDE, location.getAltitude()); } if (location.hasBearing()) { coordinates.put(PARAM_HEADING, location.getBearing()); } if (location.hasSpeed()) { coordinates.put(PARAM_SPEED, location.getSpeed()); } parameters.putString(PARAM_COORDINATES, coordinates.toString()); // min confidence level ConfidenceLevel minConfidenceLevel = request.getMinConfidenceLevel(); if (minConfidenceLevel == ConfidenceLevel.LOW || minConfidenceLevel == ConfidenceLevel.MEDIUM || minConfidenceLevel == ConfidenceLevel.HIGH) { String minConfidenceLevelString = minConfidenceLevel.toString().toLowerCase(Locale.US); parameters.putString(PARAM_MIN_CONFIDENCE_LEVEL, minConfidenceLevelString); } if (locationPackage != null) { // wifi JSONObject wifi = new JSONObject(); wifi.put(PARAM_ENABLED, locationPackage.isWifiScanningEnabled); WifiScanResult connectedWifi = locationPackage.connectedWifi; if (connectedWifi != null) { wifi.put(PARAM_CURRENT_CONNECTION, getWifiScanJson(connectedWifi)); } List<WifiScanResult> ambientWifi = locationPackage.ambientWifi; if (ambientWifi != null) { JSONArray array = new JSONArray(); for (WifiScanResult wifiScanResult : ambientWifi) { array.put(getWifiScanJson(wifiScanResult)); } wifi.put(PARAM_ACCESS_POINTS, array); } parameters.putString(PARAM_WIFI, wifi.toString()); // bluetooth JSONObject bluetooth = new JSONObject(); bluetooth.put(PARAM_ENABLED, locationPackage.isBluetoothScanningEnabled); List<BluetoothScanResult> bluetoothScanResults = locationPackage.ambientBluetoothLe; if (bluetoothScanResults != null) { JSONArray array = new JSONArray(); for (BluetoothScanResult bluetoothScanResult : bluetoothScanResults) { JSONObject bluetoothData = new JSONObject(); bluetoothData.put(PARAM_PAYLOAD, bluetoothScanResult.payload); bluetoothData.put(PARAM_RSSI, bluetoothScanResult.rssi); array.put(bluetoothData); } bluetooth.put(PARAM_SCANS, array); } parameters.putString(PARAM_BLUETOOTH, bluetooth.toString()); } return parameters; } catch (JSONException ex) { throw new FacebookException(ex); } } private static JSONObject getWifiScanJson(WifiScanResult wifiScanResult) throws JSONException { JSONObject wifiData = new JSONObject(); wifiData.put(PARAM_MAC_ADDRESS, wifiScanResult.bssid); wifiData.put(PARAM_SSID, wifiScanResult.ssid); wifiData.put(PARAM_SIGNAL_STRENGTH, wifiScanResult.rssi); wifiData.put(PARAM_FREQUENCY, wifiScanResult.frequency); return wifiData; } private static LocationError getLocationError(ScannerException.Type type) { if (type == ScannerException.Type.PERMISSION_DENIED) { return LocationError.LOCATION_PERMISSION_DENIED; } else if (type == ScannerException.Type.DISABLED) { return LocationError.LOCATION_SERVICES_DISABLED; } else if (type == ScannerException.Type.TIMEOUT) { return LocationError.LOCATION_TIMEOUT; } return LocationError.UNKNOWN_ERROR; } }