• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.service;
18 
19 import android.app.IntentService;
20 import android.app.Notification;
21 import android.app.PendingIntent;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.graphics.Bitmap;
26 import android.location.Location;
27 import android.support.v4.app.NotificationCompat;
28 import android.support.v4.app.NotificationManagerCompat;
29 import android.support.v4.content.LocalBroadcastManager;
30 import android.util.Log;
31 
32 import com.bumptech.glide.Glide;
33 import com.bumptech.glide.load.engine.DiskCacheStrategy;
34 import com.example.android.xyztouristattractions.R;
35 import com.example.android.xyztouristattractions.common.Attraction;
36 import com.example.android.xyztouristattractions.common.Constants;
37 import com.example.android.xyztouristattractions.common.Utils;
38 import com.example.android.xyztouristattractions.provider.TouristAttractions;
39 import com.example.android.xyztouristattractions.ui.DetailActivity;
40 import com.google.android.gms.common.ConnectionResult;
41 import com.google.android.gms.common.api.GoogleApiClient;
42 import com.google.android.gms.location.FusedLocationProviderApi;
43 import com.google.android.gms.location.Geofence;
44 import com.google.android.gms.location.GeofencingEvent;
45 import com.google.android.gms.location.LocationRequest;
46 import com.google.android.gms.location.LocationServices;
47 import com.google.android.gms.maps.model.LatLng;
48 import com.google.android.gms.wearable.DataApi;
49 import com.google.android.gms.wearable.DataMap;
50 import com.google.android.gms.wearable.PutDataMapRequest;
51 import com.google.android.gms.wearable.PutDataRequest;
52 import com.google.android.gms.wearable.Wearable;
53 
54 import java.util.ArrayList;
55 import java.util.Date;
56 import java.util.HashMap;
57 import java.util.Iterator;
58 import java.util.List;
59 import java.util.concurrent.ExecutionException;
60 import java.util.concurrent.TimeUnit;
61 
62 import static com.example.android.xyztouristattractions.provider.TouristAttractions.ATTRACTIONS;
63 import static com.google.android.gms.location.LocationServices.FusedLocationApi;
64 import static com.google.android.gms.location.LocationServices.GeofencingApi;
65 
66 /**
67  * A utility IntentService, used for a variety of asynchronous background
68  * operations that do not necessarily need to be tied to a UI.
69  */
70 public class UtilityService extends IntentService {
71     private static final String TAG = UtilityService.class.getSimpleName();
72 
73     public static final String ACTION_GEOFENCE_TRIGGERED = "geofence_triggered";
74     private static final String ACTION_LOCATION_UPDATED = "location_updated";
75     private static final String ACTION_REQUEST_LOCATION = "request_location";
76     private static final String ACTION_ADD_GEOFENCES = "add_geofences";
77     private static final String ACTION_CLEAR_NOTIFICATION = "clear_notification";
78     private static final String ACTION_CLEAR_REMOTE_NOTIFICATIONS = "clear_remote_notifications";
79     private static final String ACTION_FAKE_UPDATE = "fake_update";
80     private static final String EXTRA_TEST_MICROAPP = "test_microapp";
81 
getLocationUpdatedIntentFilter()82     public static IntentFilter getLocationUpdatedIntentFilter() {
83         return new IntentFilter(UtilityService.ACTION_LOCATION_UPDATED);
84     }
85 
triggerWearTest(Context context, boolean microApp)86     public static void triggerWearTest(Context context, boolean microApp) {
87         Intent intent = new Intent(context, UtilityService.class);
88         intent.setAction(UtilityService.ACTION_FAKE_UPDATE);
89         intent.putExtra(EXTRA_TEST_MICROAPP, microApp);
90         context.startService(intent);
91     }
92 
addGeofences(Context context)93     public static void addGeofences(Context context) {
94         Intent intent = new Intent(context, UtilityService.class);
95         intent.setAction(UtilityService.ACTION_ADD_GEOFENCES);
96         context.startService(intent);
97     }
98 
requestLocation(Context context)99     public static void requestLocation(Context context) {
100         Intent intent = new Intent(context, UtilityService.class);
101         intent.setAction(UtilityService.ACTION_REQUEST_LOCATION);
102         context.startService(intent);
103     }
104 
clearNotification(Context context)105     public static void clearNotification(Context context) {
106         Intent intent = new Intent(context, UtilityService.class);
107         intent.setAction(UtilityService.ACTION_CLEAR_NOTIFICATION);
108         context.startService(intent);
109     }
110 
getClearRemoteNotificationsIntent(Context context)111     public static Intent getClearRemoteNotificationsIntent(Context context) {
112         Intent intent = new Intent(context, UtilityService.class);
113         intent.setAction(UtilityService.ACTION_CLEAR_REMOTE_NOTIFICATIONS);
114         return intent;
115     }
116 
UtilityService()117     public UtilityService() {
118         super(TAG);
119     }
120 
121     @Override
onHandleIntent(Intent intent)122     protected void onHandleIntent(Intent intent) {
123         String action = intent != null ? intent.getAction() : null;
124         if (ACTION_ADD_GEOFENCES.equals(action)) {
125             addGeofencesInternal();
126         } else if (ACTION_GEOFENCE_TRIGGERED.equals(action)) {
127             geofenceTriggered(intent);
128         } else if (ACTION_REQUEST_LOCATION.equals(action)) {
129             requestLocationInternal();
130         } else if (ACTION_LOCATION_UPDATED.equals(action)) {
131             locationUpdated(intent);
132         } else if (ACTION_CLEAR_NOTIFICATION.equals(action)) {
133             clearNotificationInternal();
134         } else if (ACTION_CLEAR_REMOTE_NOTIFICATIONS.equals(action)) {
135             clearRemoteNotifications();
136         } else if (ACTION_FAKE_UPDATE.equals(action)) {
137             LatLng currentLocation = Utils.getLocation(this);
138 
139             // If location unknown use test city, otherwise use closest city
140             String city = currentLocation == null ? TouristAttractions.TEST_CITY :
141                     TouristAttractions.getClosestCity(currentLocation);
142 
143             showNotification(city,
144                     intent.getBooleanExtra(EXTRA_TEST_MICROAPP, Constants.USE_MICRO_APP));
145         }
146     }
147 
148     /**
149      * Add geofences using Play Services
150      */
addGeofencesInternal()151     private void addGeofencesInternal() {
152         Log.v(TAG, ACTION_ADD_GEOFENCES);
153         GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
154                 .addApi(LocationServices.API)
155                 .build();
156 
157         // It's OK to use blockingConnect() here as we are running in an
158         // IntentService that executes work on a separate (background) thread.
159         ConnectionResult connectionResult = googleApiClient.blockingConnect(
160                 Constants.GOOGLE_API_CLIENT_TIMEOUT_S, TimeUnit.SECONDS);
161 
162         if (connectionResult.isSuccess() && googleApiClient.isConnected()) {
163             PendingIntent pendingIntent = PendingIntent.getBroadcast(
164                     this, 0, new Intent(this, UtilityReceiver.class), 0);
165             GeofencingApi.addGeofences(googleApiClient,
166                     TouristAttractions.getGeofenceList(), pendingIntent);
167             googleApiClient.disconnect();
168         } else {
169             Log.e(TAG, String.format(Constants.GOOGLE_API_CLIENT_ERROR_MSG,
170                     connectionResult.getErrorCode()));
171         }
172     }
173 
174     /**
175      * Called when a geofence is triggered
176      */
geofenceTriggered(Intent intent)177     private void geofenceTriggered(Intent intent) {
178         Log.v(TAG, ACTION_GEOFENCE_TRIGGERED);
179 
180         // Check if geofences are enabled
181         boolean geofenceEnabled = Utils.getGeofenceEnabled(this);
182 
183         // Extract the geofences from the intent
184         GeofencingEvent event = GeofencingEvent.fromIntent(intent);
185         List<Geofence> geofences = event.getTriggeringGeofences();
186 
187         if (geofenceEnabled && geofences != null && geofences.size() > 0) {
188             if (event.getGeofenceTransition() == Geofence.GEOFENCE_TRANSITION_ENTER) {
189                 // Trigger the notification based on the first geofence
190                 showNotification(geofences.get(0).getRequestId(), Constants.USE_MICRO_APP);
191             } else if (event.getGeofenceTransition() == Geofence.GEOFENCE_TRANSITION_EXIT) {
192                 // Clear notifications
193                 clearNotificationInternal();
194                 clearRemoteNotifications();
195             }
196         }
197         UtilityReceiver.completeWakefulIntent(intent);
198     }
199 
200     /**
201      * Called when a location update is requested
202      */
requestLocationInternal()203     private void requestLocationInternal() {
204         Log.v(TAG, ACTION_REQUEST_LOCATION);
205         GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
206                 .addApi(LocationServices.API)
207                 .build();
208 
209         // It's OK to use blockingConnect() here as we are running in an
210         // IntentService that executes work on a separate (background) thread.
211         ConnectionResult connectionResult = googleApiClient.blockingConnect(
212                 Constants.GOOGLE_API_CLIENT_TIMEOUT_S, TimeUnit.SECONDS);
213 
214         if (connectionResult.isSuccess() && googleApiClient.isConnected()) {
215 
216             Intent locationUpdatedIntent = new Intent(this, UtilityService.class);
217             locationUpdatedIntent.setAction(ACTION_LOCATION_UPDATED);
218 
219             // Send last known location out first if available
220             Location location = FusedLocationApi.getLastLocation(googleApiClient);
221             if (location != null) {
222                 Intent lastLocationIntent = new Intent(locationUpdatedIntent);
223                 lastLocationIntent.putExtra(
224                         FusedLocationProviderApi.KEY_LOCATION_CHANGED, location);
225                 startService(lastLocationIntent);
226             }
227 
228             // Request new location
229             LocationRequest mLocationRequest = new LocationRequest()
230                     .setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
231             FusedLocationApi.requestLocationUpdates(
232                     googleApiClient, mLocationRequest,
233                     PendingIntent.getService(this, 0, locationUpdatedIntent, 0));
234 
235             googleApiClient.disconnect();
236         } else {
237             Log.e(TAG, String.format(Constants.GOOGLE_API_CLIENT_ERROR_MSG,
238                     connectionResult.getErrorCode()));
239         }
240     }
241 
242     /**
243      * Called when the location has been updated
244      */
locationUpdated(Intent intent)245     private void locationUpdated(Intent intent) {
246         Log.v(TAG, ACTION_LOCATION_UPDATED);
247 
248         // Extra new location
249         Location location =
250                 intent.getParcelableExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED);
251 
252         if (location != null) {
253             LatLng latLngLocation = new LatLng(location.getLatitude(), location.getLongitude());
254 
255             // Store in a local preference as well
256             Utils.storeLocation(this, latLngLocation);
257 
258             // Send a local broadcast so if an Activity is open it can respond
259             // to the updated location
260             LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
261         }
262     }
263 
264     /**
265      * Clears the local device notification
266      */
clearNotificationInternal()267     private void clearNotificationInternal() {
268         Log.v(TAG, ACTION_CLEAR_NOTIFICATION);
269         NotificationManagerCompat.from(this).cancel(Constants.MOBILE_NOTIFICATION_ID);
270     }
271 
272     /**
273      * Clears remote device notifications using the Wearable message API
274      */
clearRemoteNotifications()275     private void clearRemoteNotifications() {
276         Log.v(TAG, ACTION_CLEAR_REMOTE_NOTIFICATIONS);
277         GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
278                 .addApi(Wearable.API)
279                 .build();
280 
281         // It's OK to use blockingConnect() here as we are running in an
282         // IntentService that executes work on a separate (background) thread.
283         ConnectionResult connectionResult = googleApiClient.blockingConnect(
284                 Constants.GOOGLE_API_CLIENT_TIMEOUT_S, TimeUnit.SECONDS);
285 
286         if (connectionResult.isSuccess() && googleApiClient.isConnected()) {
287 
288             // Loop through all nodes and send a clear notification message
289             Iterator<String> itr = Utils.getNodes(googleApiClient).iterator();
290             while (itr.hasNext()) {
291                 Wearable.MessageApi.sendMessage(
292                         googleApiClient, itr.next(), Constants.CLEAR_NOTIFICATIONS_PATH, null);
293             }
294             googleApiClient.disconnect();
295         }
296     }
297 
298 
299     /**
300      * Show the notification. Either the regular notification with wearable features
301      * added to enhance, or trigger the full micro app on the wearable.
302      *
303      * @param cityId The city to trigger the notification for
304      * @param microApp If the micro app should be triggered or just enhanced notifications
305      */
showNotification(String cityId, boolean microApp)306     private void showNotification(String cityId, boolean microApp) {
307 
308         List<Attraction> attractions = ATTRACTIONS.get(cityId);
309 
310         if (microApp) {
311             // If micro app we first need to transfer some data over
312             sendDataToWearable(attractions);
313         }
314 
315         // The first (closest) tourist attraction
316         Attraction attraction = attractions.get(0);
317 
318         // Limit attractions to send
319         int count = attractions.size() > Constants.MAX_ATTRACTIONS ?
320                 Constants.MAX_ATTRACTIONS : attractions.size();
321 
322         // Pull down the tourist attraction images from the network and store
323         HashMap<String, Bitmap> bitmaps = new HashMap<>();
324         try {
325             for (int i = 0; i < count; i++) {
326                 bitmaps.put(attractions.get(i).name,
327                         Glide.with(this)
328                                 .load(attractions.get(i).imageUrl)
329                                 .asBitmap()
330                                 .diskCacheStrategy(DiskCacheStrategy.SOURCE)
331                                 .into(Constants.WEAR_IMAGE_SIZE, Constants.WEAR_IMAGE_SIZE)
332                                 .get());
333             }
334         } catch (InterruptedException | ExecutionException e) {
335             Log.e(TAG, "Error fetching image from network: " + e);
336         }
337 
338         // The intent to trigger when the notification is tapped
339         PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
340                 DetailActivity.getLaunchIntent(this, attraction.name),
341                 PendingIntent.FLAG_UPDATE_CURRENT);
342 
343         // The intent to trigger when the notification is dismissed, in this case
344         // we want to clear remote notifications as well
345         PendingIntent deletePendingIntent =
346                 PendingIntent.getService(this, 0, getClearRemoteNotificationsIntent(this), 0);
347 
348         // Construct the main notification
349         NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
350                 .setStyle(new NotificationCompat.BigPictureStyle()
351                                 .bigPicture(bitmaps.get(attraction.name))
352                                 .setBigContentTitle(attraction.name)
353                                 .setSummaryText(getString(R.string.nearby_attraction))
354                 )
355                 .setLocalOnly(microApp)
356                 .setContentTitle(attraction.name)
357                 .setContentText(getString(R.string.nearby_attraction))
358                 .setSmallIcon(R.drawable.ic_stat_maps_pin_drop)
359                 .setContentIntent(pendingIntent)
360                 .setDeleteIntent(deletePendingIntent)
361                 .setColor(getResources().getColor(R.color.colorPrimary))
362                 .setCategory(Notification.CATEGORY_RECOMMENDATION)
363                 .setAutoCancel(true);
364 
365         if (!microApp) {
366             // If not a micro app, create some wearable pages for
367             // the other nearby tourist attractions.
368             ArrayList<Notification> pages = new ArrayList<Notification>();
369             for (int i = 1; i < count; i++) {
370 
371                 // Calculate the distance from current location to tourist attraction
372                 String distance = Utils.formatDistanceBetween(
373                         Utils.getLocation(this), attractions.get(i).location);
374 
375                 // Construct the notification and add it as a page
376                 pages.add(new NotificationCompat.Builder(this)
377                         .setContentTitle(attractions.get(i).name)
378                         .setContentText(distance)
379                         .setSmallIcon(R.drawable.ic_stat_maps_pin_drop)
380                         .extend(new NotificationCompat.WearableExtender()
381                                 .setBackground(bitmaps.get(attractions.get(i).name))
382                         )
383                         .build());
384             }
385             builder.extend(new NotificationCompat.WearableExtender().addPages(pages));
386         }
387 
388         // Trigger the notification
389         NotificationManagerCompat.from(this).notify(
390                 Constants.MOBILE_NOTIFICATION_ID, builder.build());
391     }
392 
393     /**
394      * Transfer the required data over to the wearable
395      * @param attractions list of attraction data to transfer over
396      */
sendDataToWearable(List<Attraction> attractions)397     private void sendDataToWearable(List<Attraction> attractions) {
398         GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
399                 .addApi(Wearable.API)
400                 .build();
401 
402         // It's OK to use blockingConnect() here as we are running in an
403         // IntentService that executes work on a separate (background) thread.
404         ConnectionResult connectionResult = googleApiClient.blockingConnect(
405                 Constants.GOOGLE_API_CLIENT_TIMEOUT_S, TimeUnit.SECONDS);
406 
407         // Limit attractions to send
408         int count = attractions.size() > Constants.MAX_ATTRACTIONS ?
409                 Constants.MAX_ATTRACTIONS : attractions.size();
410 
411         ArrayList<DataMap> attractionsData = new ArrayList<>(count);
412 
413         for (int i = 0; i < count; i++) {
414             Attraction attraction = attractions.get(i);
415 
416             Bitmap image = null;
417             Bitmap secondaryImage = null;
418 
419             try {
420                 // Fetch and resize attraction image bitmap
421                 image = Glide.with(this)
422                         .load(attraction.imageUrl)
423                         .asBitmap()
424                         .diskCacheStrategy(DiskCacheStrategy.SOURCE)
425                         .into(Constants.WEAR_IMAGE_SIZE_PARALLAX_WIDTH, Constants.WEAR_IMAGE_SIZE)
426                         .get();
427 
428                 secondaryImage = Glide.with(this)
429                         .load(attraction.secondaryImageUrl)
430                         .asBitmap()
431                         .diskCacheStrategy(DiskCacheStrategy.SOURCE)
432                         .into(Constants.WEAR_IMAGE_SIZE_PARALLAX_WIDTH, Constants.WEAR_IMAGE_SIZE)
433                         .get();
434             } catch (InterruptedException | ExecutionException e) {
435                 Log.e(TAG, "Exception loading bitmap from network");
436             }
437 
438             if (image != null && secondaryImage != null) {
439 
440                 DataMap attractionData = new DataMap();
441 
442                 String distance = Utils.formatDistanceBetween(
443                         Utils.getLocation(this), attraction.location);
444 
445                 attractionData.putString(Constants.EXTRA_TITLE, attraction.name);
446                 attractionData.putString(Constants.EXTRA_DESCRIPTION, attraction.description);
447                 attractionData.putDouble(
448                         Constants.EXTRA_LOCATION_LAT, attraction.location.latitude);
449                 attractionData.putDouble(
450                         Constants.EXTRA_LOCATION_LNG, attraction.location.longitude);
451                 attractionData.putString(Constants.EXTRA_DISTANCE, distance);
452                 attractionData.putString(Constants.EXTRA_CITY, attraction.city);
453                 attractionData.putAsset(Constants.EXTRA_IMAGE,
454                         Utils.createAssetFromBitmap(image));
455                 attractionData.putAsset(Constants.EXTRA_IMAGE_SECONDARY,
456                         Utils.createAssetFromBitmap(secondaryImage));
457 
458                 attractionsData.add(attractionData);
459             }
460         }
461 
462         if (connectionResult.isSuccess() && googleApiClient.isConnected()
463                 && attractionsData.size() > 0) {
464 
465             PutDataMapRequest dataMap = PutDataMapRequest.create(Constants.ATTRACTION_PATH);
466             dataMap.getDataMap().putDataMapArrayList(Constants.EXTRA_ATTRACTIONS, attractionsData);
467             dataMap.getDataMap().putLong(Constants.EXTRA_TIMESTAMP, new Date().getTime());
468             PutDataRequest request = dataMap.asPutDataRequest();
469 
470             // Send the data over
471             DataApi.DataItemResult result =
472                     Wearable.DataApi.putDataItem(googleApiClient, request).await();
473 
474             if (!result.getStatus().isSuccess()) {
475                 Log.e(TAG, String.format("Error sending data using DataApi (error code = %d)",
476                         result.getStatus().getStatusCode()));
477             }
478 
479         } else {
480             Log.e(TAG, String.format(Constants.GOOGLE_API_CLIENT_ERROR_MSG,
481                     connectionResult.getErrorCode()));
482         }
483         googleApiClient.disconnect();
484     }
485 }
486