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