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