Home | History | Annotate | Download | only in service
      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 
     82     public static IntentFilter getLocationUpdatedIntentFilter() {
     83         return new IntentFilter(UtilityService.ACTION_LOCATION_UPDATED);
     84     }
     85 
     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 
     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 
     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 
    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 
    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 
    117     public UtilityService() {
    118         super(TAG);
    119     }
    120 
    121     @Override
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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