Java tutorial
/* * Copyright 2014 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gsoc.ijosa.liquidgalaxycontroller.PW; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Color; import android.os.Binder; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; import android.util.Log; import android.view.View; import android.widget.RemoteViews; import com.gsoc.ijosa.liquidgalaxycontroller.LGPCAdminActivity; import com.gsoc.ijosa.liquidgalaxycontroller.PW.collection.PhysicalWebCollection; import com.gsoc.ijosa.liquidgalaxycontroller.PW.collection.PhysicalWebCollectionException; import com.gsoc.ijosa.liquidgalaxycontroller.PW.collection.PwPair; import com.gsoc.ijosa.liquidgalaxycontroller.PW.collection.PwsResult; import com.gsoc.ijosa.liquidgalaxycontroller.PW.collection.PwsResultCallback; import com.gsoc.ijosa.liquidgalaxycontroller.PW.collection.PwsResultIconCallback; import com.gsoc.ijosa.liquidgalaxycontroller.PW.collection.UrlDevice; import com.gsoc.ijosa.liquidgalaxycontroller.R; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; /** * This is a service that scans for nearby Physical Web Objects. * It is created by MainActivity. * It finds nearby ble beacons, * and stores a count of them. * It also listens for screen on/off events * and start/stops the scanning accordingly. * It also silently issues a notification * informing the user of nearby beacons. * As beacons are found and lost, * the notification is updated to reflect * the current number of nearby beacons. */ public class UrlDeviceDiscoveryService extends Service implements UrlDeviceDiscoverer.UrlDeviceDiscoveryCallback { private static final String TAG = "UrlDeviceDisServ"; private static final String NOTIFICATION_GROUP_KEY = "URI_BEACON_NOTIFICATIONS"; private static final String PREFS_VERSION_KEY = "prefs_version"; private static final String SCAN_START_TIME_KEY = "scan_start_time"; private static final String PW_COLLECTION_KEY = "pw_collection"; private static final int PREFS_VERSION = 2; private static final int NEAREST_BEACON_NOTIFICATION_ID = 23; private static final int SECOND_NEAREST_BEACON_NOTIFICATION_ID = 24; private static final int SUMMARY_NOTIFICATION_ID = 25; private static final int NON_LOLLIPOP_NOTIFICATION_TITLE_COLOR = Color.parseColor("#ffffff"); private static final int NON_LOLLIPOP_NOTIFICATION_URL_COLOR = Color.parseColor("#999999"); private static final int NON_LOLLIPOP_NOTIFICATION_SNIPPET_COLOR = Color.parseColor("#999999"); private static final int NOTIFICATION_PRIORITY = NotificationCompat.PRIORITY_MIN; private static final int NOTIFICATION_VISIBILITY = NotificationCompat.VISIBILITY_PUBLIC; private static final long FIRST_SCAN_TIME_MILLIS = TimeUnit.SECONDS.toMillis(2); private static final long SECOND_SCAN_TIME_MILLIS = TimeUnit.SECONDS.toMillis(10); private static final long SCAN_STALE_TIME_MILLIS = TimeUnit.MINUTES.toMillis(2); private boolean mCanUpdateNotifications = false; private boolean mSecondScanComplete = false; private boolean mIsBound = false; private long mScanStartTime; private Handler mHandler; private NotificationManagerCompat mNotificationManager; private List<UrlDeviceDiscoverer> mUrlDeviceDiscoverers; private List<UrlDeviceDiscoveryListener> mUrlDeviceDiscoveryListeners; private PhysicalWebCollection mPwCollection; // Notification of urls happens as follows: // 0. Begin scan // 1. Delete notification, show top two urls (mFirstScanTimeout) // 2. Show each new url as it comes in, if it's in the top two // 3. Stop scanning if no clients are subscribed (mSecondScanTimeout) private Runnable mFirstScanTimeout = new Runnable() { @Override public void run() { mCanUpdateNotifications = true; updateNotifications(); } }; private Runnable mSecondScanTimeout = new Runnable() { @Override public void run() { mSecondScanComplete = true; if (!mIsBound) { stopSelf(); } } }; private IBinder mBinder = new LocalBinder(); public UrlDeviceDiscoveryService() { } private void initialize() { mNotificationManager = NotificationManagerCompat.from(this); mUrlDeviceDiscoverers = new ArrayList<>(); // disable mDNS PWO discovery for pre-M devices if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { mUrlDeviceDiscoverers.add(new MdnsUrlDeviceDiscoverer(this)); } mUrlDeviceDiscoverers.add(new SsdpUrlDeviceDiscoverer(this)); mUrlDeviceDiscoverers.add(new BleUrlDeviceDiscoverer(this)); for (UrlDeviceDiscoverer urlDeviceDiscoverer : mUrlDeviceDiscoverers) { urlDeviceDiscoverer.setCallback(this); } mUrlDeviceDiscoveryListeners = new ArrayList<>(); mHandler = new Handler(); mPwCollection = new PhysicalWebCollection(); mCanUpdateNotifications = false; } private void restoreCache() { // Make sure we are trying to load the right version of the cache String preferencesKey = getString(R.string.discovery_service_prefs_key); SharedPreferences prefs = getSharedPreferences(preferencesKey, Context.MODE_PRIVATE); int prefsVersion = prefs.getInt(PREFS_VERSION_KEY, 0); long now = new Date().getTime(); if (prefsVersion != PREFS_VERSION) { mScanStartTime = now; return; } // Don't load the cache if it's stale mScanStartTime = prefs.getLong(SCAN_START_TIME_KEY, 0); if (now - mScanStartTime >= SCAN_STALE_TIME_MILLIS) { mScanStartTime = now; return; } // Restore the cached metadata try { JSONObject serializedCollection = new JSONObject(prefs.getString(PW_COLLECTION_KEY, null)); mPwCollection = PhysicalWebCollection.jsonDeserialize(serializedCollection); } catch (JSONException e) { Log.e(TAG, "Could not restore collection cache", e); } catch (PhysicalWebCollectionException e) { Log.e(TAG, "Could not restore collection cache", e); } } @Override public void onCreate() { super.onCreate(); initialize(); restoreCache(); mNotificationManager.cancelAll(); mHandler.postDelayed(mFirstScanTimeout, FIRST_SCAN_TIME_MILLIS); mHandler.postDelayed(mSecondScanTimeout, SECOND_SCAN_TIME_MILLIS); for (UrlDeviceDiscoverer urlDeviceDiscoverer : mUrlDeviceDiscoverers) { urlDeviceDiscoverer.startScan(); } } @Override public IBinder onBind(Intent intent) { mIsBound = true; return mBinder; } @Override public boolean onUnbind(Intent intent) { mIsBound = false; if (mSecondScanComplete) { stopSelf(); } // true ensures onRebind is called on succcessive binds return true; } @Override public void onRebind(Intent intent) { mIsBound = true; } private void saveCache() throws JSONException { // Write the PW Collection String preferencesKey = getString(R.string.discovery_service_prefs_key); SharedPreferences prefs = getSharedPreferences(preferencesKey, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putInt(PREFS_VERSION_KEY, PREFS_VERSION); editor.putLong(SCAN_START_TIME_KEY, mScanStartTime); editor.putString(PW_COLLECTION_KEY, mPwCollection.jsonSerialize().toString()); editor.apply(); } @Override public void onDestroy() { Log.d(TAG, "onDestroy: service exiting"); // Stop the scanners mHandler.removeCallbacks(mFirstScanTimeout); mHandler.removeCallbacks(mSecondScanTimeout); for (UrlDeviceDiscoverer urlDeviceDiscoverer : mUrlDeviceDiscoverers) { urlDeviceDiscoverer.stopScan(); } try { saveCache(); } catch (JSONException e) { e.printStackTrace(); } super.onDestroy(); } @Override public void onUrlDeviceDiscovered(UrlDevice urlDevice) { mPwCollection.addUrlDevice(urlDevice); mPwCollection.fetchPwsResults(new PwsResultCallback() { long mPwsTripTimeMillis = 0; @Override public void onPwsResult(PwsResult pwsResult) { PwsResult replacement = null; try { replacement = new Utils.PwsResultBuilder(pwsResult) .setPwsTripTimeMillis(pwsResult, mPwsTripTimeMillis).build(); } catch (JSONException e) { e.printStackTrace(); } mPwCollection.addMetadata(replacement); triggerCallback(); updateNotifications(); } @Override public void onPwsResultAbsent(String url) { triggerCallback(); } @Override public void onPwsResultError(Collection<String> urls, int httpResponseCode, Exception e) { Log.d(TAG, "PwsResultError: " + httpResponseCode + " ", e); triggerCallback(); } @Override public void onResponseReceived(long durationMillis) { mPwsTripTimeMillis = durationMillis; } }, new PwsResultIconCallback() { @Override public void onIcon(byte[] icon) { triggerCallback(); } @Override public void onError(int httpResponseCode, Exception e) { Log.d(TAG, "PwsResultError: " + httpResponseCode + " ", e); triggerCallback(); } }); triggerCallback(); } private void triggerCallback() { for (UrlDeviceDiscoveryListener urlDeviceDiscoveryListener : mUrlDeviceDiscoveryListeners) { urlDeviceDiscoveryListener.onUrlDeviceDiscoveryUpdate(); } } /** * Create a new set of notifications or update those existing. */ private void updateNotifications() { if (!mCanUpdateNotifications) { return; } List<PwPair> pwPairs = mPwCollection.getGroupedPwPairsSortedByRank(); // If no beacons have been found if (pwPairs.size() == 0) { // Remove all existing notifications mNotificationManager.cancelAll(); } else if (pwPairs.size() == 1) { updateNearbyBeaconNotification(true, pwPairs.get(0), NEAREST_BEACON_NOTIFICATION_ID); } else { // Create a summary notification for both beacon notifications. // Do this first so that we don't first show the individual notifications updateSummaryNotification(pwPairs); // Create or update a notification for second beacon updateNearbyBeaconNotification(false, pwPairs.get(1), SECOND_NEAREST_BEACON_NOTIFICATION_ID); // Create or update a notification for first beacon. Needs to be added last to show up top updateNearbyBeaconNotification(false, pwPairs.get(0), NEAREST_BEACON_NOTIFICATION_ID); } } /** * Create or update a notification with the given id for the beacon with the given address. */ private void updateNearbyBeaconNotification(boolean single, PwPair pwPair, int notificationId) { PwsResult pwsResult = pwPair.getPwsResult(); NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setSmallIcon(R.drawable.ic_notification).setLargeIcon(Utils.getBitmapIcon(mPwCollection, pwsResult)) .setContentTitle(pwsResult.getTitle()).setContentText(pwsResult.getDescription()) .setPriority(NOTIFICATION_PRIORITY) .setContentIntent(Utils.createNavigateToUrlPendingIntent(pwsResult, this)); if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Utils.isPublic(pwPair.getUrlDevice())) { builder.setVisibility(NOTIFICATION_VISIBILITY); } // For some reason if there is only one notification and you call setGroup // the notification doesn't show up on the N7 running kit kat if (!single) { builder = builder.setGroup(NOTIFICATION_GROUP_KEY); } Notification notification = builder.build(); mNotificationManager.notify(notificationId, notification); } /** * Create or update the a single notification that is a collapsed version * of the top two beacon notifications. */ private void updateSummaryNotification(List<PwPair> pwPairs) { int numNearbyBeacons = pwPairs.size(); String contentTitle = String.valueOf(numNearbyBeacons); Resources resources = getResources(); contentTitle += " " + resources.getQuantityString(R.plurals.numFoundBeacons, numNearbyBeacons, numNearbyBeacons); String contentText = getString(R.string.summary_notification_pull_down); NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setSmallIcon(R.drawable.ic_notification).setContentTitle(contentTitle).setContentText(contentText) .setSmallIcon(R.drawable.ic_notification).setGroup(NOTIFICATION_GROUP_KEY).setGroupSummary(true) .setPriority(NOTIFICATION_PRIORITY).setContentIntent(createReturnToAppPendingIntent()); if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { builder.setVisibility(NOTIFICATION_VISIBILITY); } Notification notification = builder.build(); // Create the big view for the notification (viewed by pulling down) RemoteViews remoteViews = updateSummaryNotificationRemoteViews(pwPairs); notification.bigContentView = remoteViews; mNotificationManager.notify(SUMMARY_NOTIFICATION_ID, notification); } /** * Create the big view for the summary notification. */ private RemoteViews updateSummaryNotificationRemoteViews(List<PwPair> pwPairs) { RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.notification_big_view); // Fill in the data for the top two beacon views updateSummaryNotificationRemoteViewsFirstBeacon(pwPairs.get(0), remoteViews); updateSummaryNotificationRemoteViewsSecondBeacon(pwPairs.get(1), remoteViews); // Create a pending intent that will open the physical web app PendingIntent pendingIntent = createReturnToAppPendingIntent(); remoteViews.setOnClickPendingIntent(R.id.otherBeaconsLayout, pendingIntent); return remoteViews; } private void updateSummaryNotificationRemoteViewsFirstBeacon(PwPair pwPair, RemoteViews remoteViews) { PwsResult pwsResult = pwPair.getPwsResult(); remoteViews.setImageViewBitmap(R.id.icon_firstBeacon, Utils.getBitmapIcon(mPwCollection, pwsResult)); remoteViews.setTextViewText(R.id.title_firstBeacon, pwsResult.getTitle()); remoteViews.setTextViewText(R.id.url_firstBeacon, pwsResult.getSiteUrl()); remoteViews.setTextViewText(R.id.description_firstBeacon, pwsResult.getDescription()); // Recolor notifications to have light text for non-Lollipop devices if (!(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) { remoteViews.setTextColor(R.id.title_firstBeacon, NON_LOLLIPOP_NOTIFICATION_TITLE_COLOR); remoteViews.setTextColor(R.id.url_firstBeacon, NON_LOLLIPOP_NOTIFICATION_URL_COLOR); remoteViews.setTextColor(R.id.description_firstBeacon, NON_LOLLIPOP_NOTIFICATION_SNIPPET_COLOR); } // Create an intent that will open the browser to the beacon's url // if the user taps the notification remoteViews.setOnClickPendingIntent(R.id.first_beacon_main_layout, Utils.createNavigateToUrlPendingIntent(pwsResult, this)); remoteViews.setViewVisibility(R.id.firstBeaconLayout, View.VISIBLE); } private void updateSummaryNotificationRemoteViewsSecondBeacon(PwPair pwPair, RemoteViews remoteViews) { PwsResult pwsResult = pwPair.getPwsResult(); remoteViews.setImageViewBitmap(R.id.icon_secondBeacon, Utils.getBitmapIcon(mPwCollection, pwsResult)); remoteViews.setTextViewText(R.id.title_secondBeacon, pwsResult.getTitle()); remoteViews.setTextViewText(R.id.url_secondBeacon, pwsResult.getSiteUrl()); remoteViews.setTextViewText(R.id.description_secondBeacon, pwsResult.getDescription()); // Recolor notifications to have light text for non-Lollipop devices if (!(android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) { remoteViews.setTextColor(R.id.title_secondBeacon, NON_LOLLIPOP_NOTIFICATION_TITLE_COLOR); remoteViews.setTextColor(R.id.url_secondBeacon, NON_LOLLIPOP_NOTIFICATION_URL_COLOR); remoteViews.setTextColor(R.id.description_secondBeacon, NON_LOLLIPOP_NOTIFICATION_SNIPPET_COLOR); } // Create an intent that will open the browser to the beacon's url // if the user taps the notification remoteViews.setOnClickPendingIntent(R.id.second_beacon_main_layout, Utils.createNavigateToUrlPendingIntent(pwsResult, this)); remoteViews.setViewVisibility(R.id.secondBeaconLayout, View.VISIBLE); } private PendingIntent createReturnToAppPendingIntent() { Intent intent = new Intent(this, LGPCAdminActivity.class); int requestID = (int) System.currentTimeMillis(); PendingIntent pendingIntent = PendingIntent.getActivity(this, requestID, intent, 0); return pendingIntent; } public void addCallback(UrlDeviceDiscoveryListener urlDeviceDiscoveryListener) { mUrlDeviceDiscoveryListeners.add(urlDeviceDiscoveryListener); } public void removeCallback(UrlDeviceDiscoveryListener urlDeviceDiscoveryListener) { mUrlDeviceDiscoveryListeners.remove(urlDeviceDiscoveryListener); } public long getScanStartTime() { return mScanStartTime; } public void restartScan() { for (UrlDeviceDiscoverer urlDeviceDiscoverer : mUrlDeviceDiscoverers) { urlDeviceDiscoverer.stopScan(); } mScanStartTime = new Date().getTime(); for (UrlDeviceDiscoverer urlDeviceDiscoverer : mUrlDeviceDiscoverers) { urlDeviceDiscoverer.startScan(); } } public boolean hasResults() { return !mPwCollection.getGroupedPwPairsSortedByRank().isEmpty(); } public PhysicalWebCollection getPwCollection() { return mPwCollection; } /** * Callback for subscribers to this service. */ public interface UrlDeviceDiscoveryListener { void onUrlDeviceDiscoveryUpdate(); } /** * Binder class for getting connections to the service. */ public class LocalBinder extends Binder { public UrlDeviceDiscoveryService getServiceInstance() { return UrlDeviceDiscoveryService.this; } } }