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 
    154         if (!Utils.checkFineLocationPermission(this)) {
    155             return;
    156         }
    157 
    158         GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
    159                 .addApi(LocationServices.API)
    160                 .build();
    161 
    162         // It's OK to use blockingConnect() here as we are running in an
    163         // IntentService that executes work on a separate (background) thread.
    164         ConnectionResult connectionResult = googleApiClient.blockingConnect(
    165                 Constants.GOOGLE_API_CLIENT_TIMEOUT_S, TimeUnit.SECONDS);
    166 
    167         if (connectionResult.isSuccess() && googleApiClient.isConnected()) {
    168             PendingIntent pendingIntent = PendingIntent.getBroadcast(
    169                     this, 0, new Intent(this, UtilityReceiver.class), 0);
    170             GeofencingApi.addGeofences(googleApiClient,
    171                     TouristAttractions.getGeofenceList(), pendingIntent);
    172             googleApiClient.disconnect();
    173         } else {
    174             Log.e(TAG, String.format(Constants.GOOGLE_API_CLIENT_ERROR_MSG,
    175                     connectionResult.getErrorCode()));
    176         }
    177     }
    178 
    179     /**
    180      * Called when a geofence is triggered
    181      */
    182     private void geofenceTriggered(Intent intent) {
    183         Log.v(TAG, ACTION_GEOFENCE_TRIGGERED);
    184 
    185         // Check if geofences are enabled
    186         boolean geofenceEnabled = Utils.getGeofenceEnabled(this);
    187 
    188         // Extract the geofences from the intent
    189         GeofencingEvent event = GeofencingEvent.fromIntent(intent);
    190         List<Geofence> geofences = event.getTriggeringGeofences();
    191 
    192         if (geofenceEnabled && geofences != null && geofences.size() > 0) {
    193             if (event.getGeofenceTransition() == Geofence.GEOFENCE_TRANSITION_ENTER) {
    194                 // Trigger the notification based on the first geofence
    195                 showNotification(geofences.get(0).getRequestId(), Constants.USE_MICRO_APP);
    196             } else if (event.getGeofenceTransition() == Geofence.GEOFENCE_TRANSITION_EXIT) {
    197                 // Clear notifications
    198                 clearNotificationInternal();
    199                 clearRemoteNotifications();
    200             }
    201         }
    202         UtilityReceiver.completeWakefulIntent(intent);
    203     }
    204 
    205     /**
    206      * Called when a location update is requested
    207      */
    208     private void requestLocationInternal() {
    209         Log.v(TAG, ACTION_REQUEST_LOCATION);
    210 
    211         if (!Utils.checkFineLocationPermission(this)) {
    212             return;
    213         }
    214 
    215         GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
    216                 .addApi(LocationServices.API)
    217                 .build();
    218 
    219         // It's OK to use blockingConnect() here as we are running in an
    220         // IntentService that executes work on a separate (background) thread.
    221         ConnectionResult connectionResult = googleApiClient.blockingConnect(
    222                 Constants.GOOGLE_API_CLIENT_TIMEOUT_S, TimeUnit.SECONDS);
    223 
    224         if (connectionResult.isSuccess() && googleApiClient.isConnected()) {
    225 
    226             Intent locationUpdatedIntent = new Intent(this, UtilityService.class);
    227             locationUpdatedIntent.setAction(ACTION_LOCATION_UPDATED);
    228 
    229             // Send last known location out first if available
    230             Location location = FusedLocationApi.getLastLocation(googleApiClient);
    231             if (location != null) {
    232                 Intent lastLocationIntent = new Intent(locationUpdatedIntent);
    233                 lastLocationIntent.putExtra(
    234                         FusedLocationProviderApi.KEY_LOCATION_CHANGED, location);
    235                 startService(lastLocationIntent);
    236             }
    237 
    238             // Request new location
    239             LocationRequest mLocationRequest = new LocationRequest()
    240                     .setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
    241             FusedLocationApi.requestLocationUpdates(
    242                     googleApiClient, mLocationRequest,
    243                     PendingIntent.getService(this, 0, locationUpdatedIntent, 0));
    244 
    245             googleApiClient.disconnect();
    246         } else {
    247             Log.e(TAG, String.format(Constants.GOOGLE_API_CLIENT_ERROR_MSG,
    248                     connectionResult.getErrorCode()));
    249         }
    250     }
    251 
    252     /**
    253      * Called when the location has been updated
    254      */
    255     private void locationUpdated(Intent intent) {
    256         Log.v(TAG, ACTION_LOCATION_UPDATED);
    257 
    258         // Extra new location
    259         Location location =
    260                 intent.getParcelableExtra(FusedLocationProviderApi.KEY_LOCATION_CHANGED);
    261 
    262         if (location != null) {
    263             LatLng latLngLocation = new LatLng(location.getLatitude(), location.getLongitude());
    264 
    265             // Store in a local preference as well
    266             Utils.storeLocation(this, latLngLocation);
    267 
    268             // Send a local broadcast so if an Activity is open it can respond
    269             // to the updated location
    270             LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
    271         }
    272     }
    273 
    274     /**
    275      * Clears the local device notification
    276      */
    277     private void clearNotificationInternal() {
    278         Log.v(TAG, ACTION_CLEAR_NOTIFICATION);
    279         NotificationManagerCompat.from(this).cancel(Constants.MOBILE_NOTIFICATION_ID);
    280     }
    281 
    282     /**
    283      * Clears remote device notifications using the Wearable message API
    284      */
    285     private void clearRemoteNotifications() {
    286         Log.v(TAG, ACTION_CLEAR_REMOTE_NOTIFICATIONS);
    287         GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
    288                 .addApi(Wearable.API)
    289                 .build();
    290 
    291         // It's OK to use blockingConnect() here as we are running in an
    292         // IntentService that executes work on a separate (background) thread.
    293         ConnectionResult connectionResult = googleApiClient.blockingConnect(
    294                 Constants.GOOGLE_API_CLIENT_TIMEOUT_S, TimeUnit.SECONDS);
    295 
    296         if (connectionResult.isSuccess() && googleApiClient.isConnected()) {
    297 
    298             // Loop through all nodes and send a clear notification message
    299             Iterator<String> itr = Utils.getNodes(googleApiClient).iterator();
    300             while (itr.hasNext()) {
    301                 Wearable.MessageApi.sendMessage(
    302                         googleApiClient, itr.next(), Constants.CLEAR_NOTIFICATIONS_PATH, null);
    303             }
    304             googleApiClient.disconnect();
    305         }
    306     }
    307 
    308 
    309     /**
    310      * Show the notification. Either the regular notification with wearable features
    311      * added to enhance, or trigger the full micro app on the wearable.
    312      *
    313      * @param cityId The city to trigger the notification for
    314      * @param microApp If the micro app should be triggered or just enhanced notifications
    315      */
    316     private void showNotification(String cityId, boolean microApp) {
    317 
    318         List<Attraction> attractions = ATTRACTIONS.get(cityId);
    319 
    320         if (microApp) {
    321             // If micro app we first need to transfer some data over
    322             sendDataToWearable(attractions);
    323         }
    324 
    325         // The first (closest) tourist attraction
    326         Attraction attraction = attractions.get(0);
    327 
    328         // Limit attractions to send
    329         int count = attractions.size() > Constants.MAX_ATTRACTIONS ?
    330                 Constants.MAX_ATTRACTIONS : attractions.size();
    331 
    332         // Pull down the tourist attraction images from the network and store
    333         HashMap<String, Bitmap> bitmaps = new HashMap<>();
    334         try {
    335             for (int i = 0; i < count; i++) {
    336                 bitmaps.put(attractions.get(i).name,
    337                         Glide.with(this)
    338                                 .load(attractions.get(i).imageUrl)
    339                                 .asBitmap()
    340                                 .diskCacheStrategy(DiskCacheStrategy.SOURCE)
    341                                 .into(Constants.WEAR_IMAGE_SIZE, Constants.WEAR_IMAGE_SIZE)
    342                                 .get());
    343             }
    344         } catch (InterruptedException | ExecutionException e) {
    345             Log.e(TAG, "Error fetching image from network: " + e);
    346         }
    347 
    348         // The intent to trigger when the notification is tapped
    349         PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
    350                 DetailActivity.getLaunchIntent(this, attraction.name),
    351                 PendingIntent.FLAG_UPDATE_CURRENT);
    352 
    353         // The intent to trigger when the notification is dismissed, in this case
    354         // we want to clear remote notifications as well
    355         PendingIntent deletePendingIntent =
    356                 PendingIntent.getService(this, 0, getClearRemoteNotificationsIntent(this), 0);
    357 
    358         // Construct the main notification
    359         NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
    360                 .setStyle(new NotificationCompat.BigPictureStyle()
    361                                 .bigPicture(bitmaps.get(attraction.name))
    362                                 .setBigContentTitle(attraction.name)
    363                                 .setSummaryText(getString(R.string.nearby_attraction))
    364                 )
    365                 .setLocalOnly(microApp)
    366                 .setContentTitle(attraction.name)
    367                 .setContentText(getString(R.string.nearby_attraction))
    368                 .setSmallIcon(R.drawable.ic_stat_maps_pin_drop)
    369                 .setContentIntent(pendingIntent)
    370                 .setDeleteIntent(deletePendingIntent)
    371                 .setColor(getResources().getColor(R.color.colorPrimary, getTheme()))
    372                 .setCategory(Notification.CATEGORY_RECOMMENDATION)
    373                 .setAutoCancel(true);
    374 
    375         if (!microApp) {
    376             // If not a micro app, create some wearable pages for
    377             // the other nearby tourist attractions.
    378             ArrayList<Notification> pages = new ArrayList<Notification>();
    379             for (int i = 1; i < count; i++) {
    380 
    381                 // Calculate the distance from current location to tourist attraction
    382                 String distance = Utils.formatDistanceBetween(
    383                         Utils.getLocation(this), attractions.get(i).location);
    384 
    385                 // Construct the notification and add it as a page
    386                 pages.add(new NotificationCompat.Builder(this)
    387                         .setContentTitle(attractions.get(i).name)
    388                         .setContentText(distance)
    389                         .setSmallIcon(R.drawable.ic_stat_maps_pin_drop)
    390                         .extend(new NotificationCompat.WearableExtender()
    391                                 .setBackground(bitmaps.get(attractions.get(i).name))
    392                         )
    393                         .build());
    394             }
    395             builder.extend(new NotificationCompat.WearableExtender().addPages(pages));
    396         }
    397 
    398         // Trigger the notification
    399         NotificationManagerCompat.from(this).notify(
    400                 Constants.MOBILE_NOTIFICATION_ID, builder.build());
    401     }
    402 
    403     /**
    404      * Transfer the required data over to the wearable
    405      * @param attractions list of attraction data to transfer over
    406      */
    407     private void sendDataToWearable(List<Attraction> attractions) {
    408         GoogleApiClient googleApiClient = new GoogleApiClient.Builder(this)
    409                 .addApi(Wearable.API)
    410                 .build();
    411 
    412         // It's OK to use blockingConnect() here as we are running in an
    413         // IntentService that executes work on a separate (background) thread.
    414         ConnectionResult connectionResult = googleApiClient.blockingConnect(
    415                 Constants.GOOGLE_API_CLIENT_TIMEOUT_S, TimeUnit.SECONDS);
    416 
    417         // Limit attractions to send
    418         int count = attractions.size() > Constants.MAX_ATTRACTIONS ?
    419                 Constants.MAX_ATTRACTIONS : attractions.size();
    420 
    421         ArrayList<DataMap> attractionsData = new ArrayList<>(count);
    422 
    423         for (int i = 0; i < count; i++) {
    424             Attraction attraction = attractions.get(i);
    425 
    426             Bitmap image = null;
    427             Bitmap secondaryImage = null;
    428 
    429             try {
    430                 // Fetch and resize attraction image bitmap
    431                 image = Glide.with(this)
    432                         .load(attraction.imageUrl)
    433                         .asBitmap()
    434                         .diskCacheStrategy(DiskCacheStrategy.SOURCE)
    435                         .into(Constants.WEAR_IMAGE_SIZE_PARALLAX_WIDTH, Constants.WEAR_IMAGE_SIZE)
    436                         .get();
    437 
    438                 secondaryImage = Glide.with(this)
    439                         .load(attraction.secondaryImageUrl)
    440                         .asBitmap()
    441                         .diskCacheStrategy(DiskCacheStrategy.SOURCE)
    442                         .into(Constants.WEAR_IMAGE_SIZE_PARALLAX_WIDTH, Constants.WEAR_IMAGE_SIZE)
    443                         .get();
    444             } catch (InterruptedException | ExecutionException e) {
    445                 Log.e(TAG, "Exception loading bitmap from network");
    446             }
    447 
    448             if (image != null && secondaryImage != null) {
    449 
    450                 DataMap attractionData = new DataMap();
    451 
    452                 String distance = Utils.formatDistanceBetween(
    453                         Utils.getLocation(this), attraction.location);
    454 
    455                 attractionData.putString(Constants.EXTRA_TITLE, attraction.name);
    456                 attractionData.putString(Constants.EXTRA_DESCRIPTION, attraction.description);
    457                 attractionData.putDouble(
    458                         Constants.EXTRA_LOCATION_LAT, attraction.location.latitude);
    459                 attractionData.putDouble(
    460                         Constants.EXTRA_LOCATION_LNG, attraction.location.longitude);
    461                 attractionData.putString(Constants.EXTRA_DISTANCE, distance);
    462                 attractionData.putString(Constants.EXTRA_CITY, attraction.city);
    463                 attractionData.putAsset(Constants.EXTRA_IMAGE,
    464                         Utils.createAssetFromBitmap(image));
    465                 attractionData.putAsset(Constants.EXTRA_IMAGE_SECONDARY,
    466                         Utils.createAssetFromBitmap(secondaryImage));
    467 
    468                 attractionsData.add(attractionData);
    469             }
    470         }
    471 
    472         if (connectionResult.isSuccess() && googleApiClient.isConnected()
    473                 && attractionsData.size() > 0) {
    474 
    475             PutDataMapRequest dataMap = PutDataMapRequest.create(Constants.ATTRACTION_PATH);
    476             dataMap.getDataMap().putDataMapArrayList(Constants.EXTRA_ATTRACTIONS, attractionsData);
    477             dataMap.getDataMap().putLong(Constants.EXTRA_TIMESTAMP, new Date().getTime());
    478             PutDataRequest request = dataMap.asPutDataRequest();
    479             request.setUrgent();
    480 
    481             // Send the data over
    482             DataApi.DataItemResult result =
    483                     Wearable.DataApi.putDataItem(googleApiClient, request).await();
    484 
    485             if (!result.getStatus().isSuccess()) {
    486                 Log.e(TAG, String.format("Error sending data using DataApi (error code = %d)",
    487                         result.getStatus().getStatusCode()));
    488             }
    489 
    490         } else {
    491             Log.e(TAG, String.format(Constants.GOOGLE_API_CLIENT_ERROR_MSG,
    492                     connectionResult.getErrorCode()));
    493         }
    494         googleApiClient.disconnect();
    495     }
    496 }
    497