1 /* 2 * Copyright 2015 Google Inc. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.xyztouristattractions.common; 18 19 import android.Manifest; 20 import android.content.Context; 21 import android.content.SharedPreferences; 22 import android.content.pm.PackageManager; 23 import android.graphics.Bitmap; 24 import android.graphics.BitmapFactory; 25 import android.graphics.Point; 26 import android.graphics.Rect; 27 import android.preference.PreferenceManager; 28 import android.support.v4.content.ContextCompat; 29 import android.util.Log; 30 import android.view.Display; 31 32 import com.google.android.gms.common.api.GoogleApiClient; 33 import com.google.android.gms.maps.model.LatLng; 34 import com.google.android.gms.wearable.Asset; 35 import com.google.android.gms.wearable.Node; 36 import com.google.android.gms.wearable.NodeApi; 37 import com.google.android.gms.wearable.Wearable; 38 import com.google.maps.android.SphericalUtil; 39 40 import java.io.ByteArrayOutputStream; 41 import java.io.InputStream; 42 import java.text.NumberFormat; 43 import java.util.Collection; 44 import java.util.HashSet; 45 46 /** 47 * This class contains shared static utility methods that both the mobile and 48 * wearable apps can use. 49 */ 50 public class Utils { 51 private static final String TAG = Utils.class.getSimpleName(); 52 53 private static final String PREFERENCES_LAT = "lat"; 54 private static final String PREFERENCES_LNG = "lng"; 55 private static final String PREFERENCES_GEOFENCE_ENABLED = "geofence"; 56 private static final String DISTANCE_KM_POSTFIX = "km"; 57 private static final String DISTANCE_M_POSTFIX = "m"; 58 59 /** 60 * Check if the app has access to fine location permission. On pre-M 61 * devices this will always return true. 62 */ checkFineLocationPermission(Context context)63 public static boolean checkFineLocationPermission(Context context) { 64 return PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission( 65 context, Manifest.permission.ACCESS_FINE_LOCATION); 66 } 67 68 /** 69 * Calculate distance between two LatLng points and format it nicely for 70 * display. As this is a sample, it only statically supports metric units. 71 * A production app should check locale and support the correct units. 72 */ formatDistanceBetween(LatLng point1, LatLng point2)73 public static String formatDistanceBetween(LatLng point1, LatLng point2) { 74 if (point1 == null || point2 == null) { 75 return null; 76 } 77 78 NumberFormat numberFormat = NumberFormat.getNumberInstance(); 79 double distance = Math.round(SphericalUtil.computeDistanceBetween(point1, point2)); 80 81 // Adjust to KM if M goes over 1000 (see javadoc of method for note 82 // on only supporting metric) 83 if (distance >= 1000) { 84 numberFormat.setMaximumFractionDigits(1); 85 return numberFormat.format(distance / 1000) + DISTANCE_KM_POSTFIX; 86 } 87 return numberFormat.format(distance) + DISTANCE_M_POSTFIX; 88 } 89 90 /** 91 * Store the location in the app preferences. 92 */ storeLocation(Context context, LatLng location)93 public static void storeLocation(Context context, LatLng location) { 94 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 95 SharedPreferences.Editor editor = prefs.edit(); 96 editor.putLong(PREFERENCES_LAT, Double.doubleToRawLongBits(location.latitude)); 97 editor.putLong(PREFERENCES_LNG, Double.doubleToRawLongBits(location.longitude)); 98 editor.apply(); 99 } 100 101 /** 102 * Fetch the location from app preferences. 103 */ getLocation(Context context)104 public static LatLng getLocation(Context context) { 105 if (!checkFineLocationPermission(context)) { 106 return null; 107 } 108 109 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 110 Long lat = prefs.getLong(PREFERENCES_LAT, Long.MAX_VALUE); 111 Long lng = prefs.getLong(PREFERENCES_LNG, Long.MAX_VALUE); 112 if (lat != Long.MAX_VALUE && lng != Long.MAX_VALUE) { 113 Double latDbl = Double.longBitsToDouble(lat); 114 Double lngDbl = Double.longBitsToDouble(lng); 115 return new LatLng(latDbl, lngDbl); 116 } 117 return null; 118 } 119 120 /** 121 * Store if geofencing triggers will show a notification in app preferences. 122 */ storeGeofenceEnabled(Context context, boolean enable)123 public static void storeGeofenceEnabled(Context context, boolean enable) { 124 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 125 SharedPreferences.Editor editor = prefs.edit(); 126 editor.putBoolean(PREFERENCES_GEOFENCE_ENABLED, enable); 127 editor.apply(); 128 } 129 130 /** 131 * Retrieve if geofencing triggers should show a notification from app preferences. 132 */ getGeofenceEnabled(Context context)133 public static boolean getGeofenceEnabled(Context context) { 134 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 135 return prefs.getBoolean(PREFERENCES_GEOFENCE_ENABLED, true); 136 } 137 138 /** 139 * Convert an asset into a bitmap object synchronously. Only call this 140 * method from a background thread (it should never be called from the 141 * main/UI thread as it blocks). 142 */ loadBitmapFromAsset(GoogleApiClient googleApiClient, Asset asset)143 public static Bitmap loadBitmapFromAsset(GoogleApiClient googleApiClient, Asset asset) { 144 if (asset == null) { 145 throw new IllegalArgumentException("Asset must be non-null"); 146 } 147 // convert asset into a file descriptor and block until it's ready 148 InputStream assetInputStream = Wearable.DataApi.getFdForAsset( 149 googleApiClient, asset).await().getInputStream(); 150 151 if (assetInputStream == null) { 152 Log.w(TAG, "Requested an unknown Asset."); 153 return null; 154 } 155 // decode the stream into a bitmap 156 return BitmapFactory.decodeStream(assetInputStream); 157 } 158 159 /** 160 * Create a wearable asset from a bitmap. 161 */ createAssetFromBitmap(Bitmap bitmap)162 public static Asset createAssetFromBitmap(Bitmap bitmap) { 163 if (bitmap != null) { 164 final ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); 165 bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteStream); 166 return Asset.createFromBytes(byteStream.toByteArray()); 167 } 168 return null; 169 } 170 171 /** 172 * Get a list of all wearable nodes that are connected synchronously. 173 * Only call this method from a background thread (it should never be 174 * called from the main/UI thread as it blocks). 175 */ getNodes(GoogleApiClient client)176 public static Collection<String> getNodes(GoogleApiClient client) { 177 Collection<String> results= new HashSet<String>(); 178 NodeApi.GetConnectedNodesResult nodes = 179 Wearable.NodeApi.getConnectedNodes(client).await(); 180 for (Node node : nodes.getNodes()) { 181 results.add(node.getId()); 182 } 183 return results; 184 } 185 186 /** 187 * Calculates the square insets on a round device. If the system insets are not set 188 * (set to 0) then the inner square of the circle is applied instead. 189 * 190 * @param display device default display 191 * @param systemInsets the system insets 192 * @return adjusted square insets for use on a round device 193 */ calculateBottomInsetsOnRoundDevice(Display display, Rect systemInsets)194 public static Rect calculateBottomInsetsOnRoundDevice(Display display, Rect systemInsets) { 195 Point size = new Point(); 196 display.getSize(size); 197 int width = size.x + systemInsets.left + systemInsets.right; 198 int height = size.y + systemInsets.top + systemInsets.bottom; 199 200 // Minimum inset to use on a round screen, calculated as a fixed percent of screen height 201 int minInset = (int) (height * Constants.WEAR_ROUND_MIN_INSET_PERCENT); 202 203 // Use system inset if it is larger than min inset, otherwise use min inset 204 int bottomInset = systemInsets.bottom > minInset ? systemInsets.bottom : minInset; 205 206 // Calculate left and right insets based on bottom inset 207 double radius = width / 2; 208 double apothem = radius - bottomInset; 209 double chord = Math.sqrt(Math.pow(radius, 2) - Math.pow(apothem, 2)) * 2; 210 int leftRightInset = (int) ((width - chord) / 2); 211 212 Log.d(TAG, "calculateBottomInsetsOnRoundDevice: " + bottomInset + ", " + leftRightInset); 213 214 return new Rect(leftRightInset, 0, leftRightInset, bottomInset); 215 } 216 } 217