Java tutorial
/** * * Copyright (c) 2013,2014 RadiusNetworks. All rights reserved. * http://www.radiusnetworks.com * * @author David G. Young * * Licensed to the Attribution Assurance License (AAL) * (adapted from the original BSD license) See the LICENSE file * distributed with this work for additional information * regarding copyright ownership. * */ package com.radiusnetworks.scavengerhunt; import android.app.Activity; import android.app.ActivityManager; import android.app.Application; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.TaskStackBuilder; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.util.Log; import com.radiusnetworks.proximity.ProximityKitBeacon; import com.radiusnetworks.proximity.ProximityKitBeaconRegion; import com.radiusnetworks.proximity.ProximityKitManager; import com.radiusnetworks.proximity.ProximityKitMonitorNotifier; import com.radiusnetworks.proximity.ProximityKitRangeNotifier; import com.radiusnetworks.proximity.ProximityKitSyncNotifier; import com.radiusnetworks.proximity.licensing.PropertiesFile; import com.radiusnetworks.proximity.model.KitBeacon; import com.radiusnetworks.scavengerhunt.assets.AssetFetcherCallback; import com.radiusnetworks.scavengerhunt.assets.CustomAssetCache; import com.radiusnetworks.scavengerhunt.assets.RemoteAssetCache; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Created by dyoung on 1/24/14. * * This is the central application class for the Scavenger Hunt. It is responsible for: * 1. Initializing ProxomityKit, which downloads all the beacons associated with the hunt * along with their configured hunt_id values and image_url values. It then starts * ranging and monitoring for these beacons, and will continue to do so even across * a reboot. * 2. Downloads all the scavenger hunt badges ("target images") needed for both the found and * not found states of each target. * 3. Updates the LoadingActivity with the status of the above download state. * 4. Once loading completes, launches the TargetCollectionActivity which is the main screen for the * hunt. * 5. Handles all ranging and monitoring callbacks for beacons. When an beacon is ranged, * this class checks to see if it matches a scavenger hunt target and awards a badge if it is * close enough. If an beacon comes into view, it sends a notification that a target is nearby. */ public class ScavengerHuntApplication extends Application implements ProximityKitMonitorNotifier, ProximityKitRangeNotifier, ProximityKitSyncNotifier { private static final String TAG = "ScavengerHuntApplication"; private static final Double MINIMUM_TRIGGER_DISTANCE_METERS = 10.0; private static final long REPEAT_NOTIF_RESTRICTED_PERIOD_MSECS = 300000; private ProximityKitManager manager; private Hunt hunt; private final long[] VIBRATOR_PATTERN = { 0l, 500l, 0l }; // start immediately, vigrate for 500ms, sleep for 0ms @SuppressWarnings("unused") private TargetCollectionActivity collectionActivity = null; private TargetItemActivity itemActivity = null; private LoadingActivity loadingActivity = null; private InstructionActivity instructionActivity = null; private RemoteAssetCache remoteAssetCache; private CustomAssetCache customAssetCache; private String loadingFailedTitle; private String loadingFailedMessage; private boolean codeNeeded; private boolean ignoreSync = true; @Override public void onCreate() { super.onCreate(); Log.d(TAG, "Application onCreate. Hunt is " + hunt); manager = ProximityKitManager.getInstanceForApplication(this); manager.setProximityKitMonitorNotifier(this); manager.setProximityKitRangeNotifier(this); manager.setProximityKitSyncNotifier(this); // Include beacon logs and shorten background polling //manager.getBeaconManager().setDebug(true); //manager.getBeaconManager().setBackgroundBetweenScanPeriod(30000); remoteAssetCache = new RemoteAssetCache(this); customAssetCache = new CustomAssetCache(this); if (!new PropertiesFile().exists()) { this.codeNeeded = true; return; } else { Log.d(TAG, "code not needed."); } } public void startPk(String code, boolean resume) { Log.d(TAG, "startPk called with code " + code); if (code != null) { if (resume) { ignoreSync = true; } else { ignoreSync = false; } Log.d(TAG, "restarting PK with code: " + code); manager.restart(code); } else { ignoreSync = false; manager.start(); // This starts ranging and monitoring for beacons defined in ProximityKit\ } } public boolean canResume() { hunt = Hunt.loadFromPreferences(this); remoteAssetCache = new RemoteAssetCache(this); Log.d(TAG, "hunt loaded from preferences at boot: " + hunt); return (hunt != null && hunt.getTargetList().size() > 0 && validateRequiredImagesPresent()); } public void startOver(Activity activity, boolean forceCodeReentry) { if (hunt != null) { hunt.reset(); hunt.saveToPreferences(this); } Log.i(TAG, "starting over"); cancelAllNotifications(); if (this.collectionActivity != null) { Log.i(TAG, "calling finish on " + this.collectionActivity); //not sure this is reliable this.collectionActivity.forceReconfigure(); // this.collectionActivity.finish(); // do this so it won't show up again on back press // this.collectionActivity = null; } Intent intent; if (forceCodeReentry && (!new PropertiesFile().exists())) { this.codeNeeded = true; Log.d(TAG, "clearing shared preferences"); SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this); String code = settings.getString("code", null); SharedPreferences.Editor editor = settings.edit(); editor.clear(); editor.putString("code", code); editor.commit(); hunt = null; remoteAssetCache = new RemoteAssetCache(this); remoteAssetCache.clear(); customAssetCache = new CustomAssetCache(this); customAssetCache.clear(); this.codeNeeded = true; intent = new Intent(activity, LoadingActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); } else { hunt.reset(); hunt.start(); this.collectionActivity.recreate(); //intent = new Intent(activity, TargetCollectionActivity.class);//InstructionActivity.class); } } public void setLoadingActivity(LoadingActivity activity) { this.loadingActivity = activity; if (this.loadingFailedTitle != null) { showFailedErrorMessage(); } } public void setInstructionActivity(InstructionActivity activity) { this.instructionActivity = activity; } // if loading dependencies fails, we want to display a message to the user // we can only do so if the loading activity has already been created // otherwise, we store the messages for display a second or so later // when that acdtivity finally launches private void dependencyLoadingFailed(String title, String message) { Log.d(TAG, "dependencyLoadingFailed"); this.loadingFailedTitle = title; this.loadingFailedMessage = message; if (this.loadingActivity != null) { showFailedErrorMessage(); } } // actually get the loading activity to display an error message to the user private void showFailedErrorMessage() { Log.d(TAG, "showFailedErrorMesage"); loadingActivity.failAndTryAgain(loadingFailedTitle, loadingFailedMessage); loadingFailedTitle = null; } @Override /** * Called whenever the Proximity Kit manager sees registered beacons. * * @param beacons a collection of <code>ProximityKitBeacon</code> instances seen in the most * recent ranging cycle. * @param region The <code>ProximityKitBeaconRegion</code> instance that was used to start * ranging for these beacons. */ public void didRangeBeaconsInRegion(Collection<ProximityKitBeacon> beacons, ProximityKitBeaconRegion region) { Log.d(TAG, "didRangeBeaconsInRegion: size=" + beacons.size() + " region=" + region); for (ProximityKitBeacon beacon : beacons) { Log.i(TAG, "ranged beacon: " + beacon.getId1() + " " + beacon.getId2() + " " + beacon.getId3()); String huntId = null; Double triggerDistanceMeters = MINIMUM_TRIGGER_DISTANCE_METERS; Map<String, String> beaconData = beacon.getAttributes(); if (beaconData != null) { huntId = beaconData.get("hunt_id"); if (beaconData.get("trigger_distance") != null) { triggerDistanceMeters = Double.parseDouble(beaconData.get("trigger_distance")); } } if (huntId == null) { Log.d(TAG, "The beacon I just saw is not part of the scavenger hunt, according to ProximityKit"); return; } if (hunt == null) { // Hunt has not been initialized from PK yet. Ignoring all beacons return; } TargetItem target = hunt.getTargetById(huntId); if (target == null) { Log.w(TAG, "The beacon I just saw has a hunt_id of " + huntId + ", but it was not part of the scavenger hunt when this app was started."); return; } if (hunt.getElapsedTime() != 0) { long timeTargetNotifLastSent = target.getTimeNotifLastSent(); long currentTimeMsecs = System.currentTimeMillis(); // Logic to determine when a local notification will be sent if ((currentTimeMsecs - timeTargetNotifLastSent) > REPEAT_NOTIF_RESTRICTED_PERIOD_MSECS && hunt.allowNotification() && (isApplicationSentToBackground(this.getApplicationContext())) && !target.isFound()) { Log.i(TAG, "Sending notification"); sendNotification(); target.setTimeNotifLastSent(currentTimeMsecs); } if (beacon.getDistance() < triggerDistanceMeters && !target.isFound()) { Log.i(TAG, "Found an item. beacon.getAccuracy(): " + beacon.getDistance()); target.setFound(true); hunt.saveToPreferences(this); if (collectionActivity != null) { collectionActivity.showItemFound(); if (hunt.everythingFound()) { // switch to FinishedActivity to show player he/she has won Log.d(TAG, "game is won"); cancelAllNotifications(); Intent i = new Intent(collectionActivity, FinishedActivity.class); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(i); } } else { Log.i(TAG, "null collection activity"); } } else { if (target.isFound()) { Log.d(TAG, "Not marking this target as found because it is already found"); } else if (beacon.getDistance() < triggerDistanceMeters) { Log.d(TAG, "Not marking this target as found because it isn't close enough. it needs to be under " + triggerDistanceMeters + " but is " + beacon.getDistance()); } } } else { Log.d(TAG, "hunt hasn't started, so all beacon detections are being ignored"); } if (itemActivity != null) { itemActivity.updateDistance(beacon, beaconData); } } } @Override public void didEnterRegion(ProximityKitBeaconRegion region) { // Called when one of the beacons defined in ProximityKit first appears Log.d(TAG, "didEnterRegion"); } @Override public void didExitRegion(ProximityKitBeaconRegion region) { // Called when one of the beacons defined in ProximityKit first disappears Log.d(TAG, "didExitRegion"); } @Override public void didDetermineStateForRegion(int i, ProximityKitBeaconRegion region) { // Called when one of the beacons defined in ProximityKit first appears or disappears Log.d(TAG, "didExitRegion"); } @Override public void didSync() { // Called when ProximityKit data are updated from the server Log.d(TAG, "proximityKit didSync. kit is " + manager.getKit()); if (ignoreSync) { Log.d(TAG, "ignoring sync"); return; } ignoreSync = true; ArrayList<TargetItem> targets = new ArrayList<TargetItem>(); Map<String, String> urlMap = new HashMap<String, String>(); Map<String, String> customStartScreenData = new HashMap<String, String>(); Map<String, String> customAssetUrlMap = new HashMap<String, String>(); for (KitBeacon beacon : manager.getKit().getBeacons()) { String huntId = beacon.getAttributes().get("hunt_id"); if (huntId != null) { TargetItem target = new TargetItem(huntId); String title = beacon.getAttributes().get("title"); if (title != null) { target.setTitle(title); Log.d(TAG, "title = " + title); } else { Log.d(TAG, "title = null"); } String description = beacon.getAttributes().get("description"); if (description != null) { target.setDescription(description); Log.d(TAG, "description = " + description); } else { Log.d(TAG, "description = null"); } targets.add(target); String imageUrl = beacon.getAttributes().get("image_url"); if (imageUrl == null) { Log.e(TAG, "ERROR: No image_url specified in ProximityKit for item with hunt_id=" + huntId); loadingActivity.codeValidationFailed(new RuntimeException( "No targets configured for the entered code. At least one beacon must be configured in ProximityKit with a hunt_id key.")); return; } else { urlMap.put("target" + huntId + "_found", variantTargetImageUrlForBaseUrlString(imageUrl, true)); urlMap.put("target" + huntId, variantTargetImageUrlForBaseUrlString(imageUrl, false)); } String splashUrl = beacon.getAttributes().get("splash_url"); Log.e(TAG, "splashUrl = " + splashUrl); //custom splash screen and instructions screen metadata String instruction_background_color = beacon.getAttributes().get("instruction_background_color"); if (instruction_background_color != null) { Log.d(TAG, "------This hunt has a custom instruction screen because the instruction_background_color is set to " + instruction_background_color); //save custom splash screen and instructions screen for later use try { customStartScreenData.put("instruction_background_color", beacon.getAttributes().get("instruction_background_color")); customStartScreenData.put("instruction_image_url", beacon.getAttributes().get("instruction_image_url")); customStartScreenData.put("instruction_start_button_name", beacon.getAttributes().get("instruction_start_button_name")); customStartScreenData.put("instruction_text_1", beacon.getAttributes().get("instruction_text_1")); customStartScreenData.put("instruction_title", beacon.getAttributes().get("instruction_title")); customStartScreenData.put("splash_url", beacon.getAttributes().get("splash_url")); customStartScreenData.put("finish_background_color", beacon.getAttributes().get("finish_background_color")); customStartScreenData.put("finish_image_url", beacon.getAttributes().get("finish_image_url")); customStartScreenData.put("finish_button_name", beacon.getAttributes().get("finish_button_name")); customStartScreenData.put("finish_text_1", beacon.getAttributes().get("finish_text_1")); customAssetUrlMap.put("instruction_image", beacon.getAttributes().get("instruction_image_url")); customAssetUrlMap.put("finish_image", beacon.getAttributes().get("finish_image_url")); customAssetUrlMap.put("splash", beacon.getAttributes().get("splash_url")); } catch (Exception e) { e.printStackTrace(); customStartScreenData = null; } } } } if (targets.size() != 0) { if (loadingActivity != null && loadingActivity.isValidatingCode()) { loadingActivity.codeValidationPassed(); } } else { if (loadingActivity != null && loadingActivity.isValidatingCode()) { loadingActivity.codeValidationFailed(new RuntimeException( "No targets configured for the entered code. At least one beacon must be configured in ProximityKit with a hunt_id key.")); return; } } // load the saved state of the hunt from the phone's persistent // storage hunt = Hunt.loadFromPreferences(this); boolean targetListChanged = hunt.getTargetList().size() != targets.size(); for (TargetItem target : hunt.getTargetList()) { boolean itemFound = false; for (TargetItem targetFromPk : targets) { if (targetFromPk.getId().equals(target.getId())) itemFound = true; } if (itemFound == false) { targetListChanged = true; Log.d(TAG, "Target with hunt_id=" + target.getId() + " is no longer in PK. Target list has changed."); } } if (customStartScreenData != null && customStartScreenData.equals(hunt.getCustomStartScreenData())) { targetListChanged = true; Log.d(TAG, "customStartScreenData.equals(savedCustomData)"); } else { Log.d(TAG, "customStartScreenData DOES NOT EQUAL savedCustomData"); } if (targetListChanged) { Log.w(TAG, "the targets in the hunt has changed from what we have in the settings. starting over"); this.hunt = (customStartScreenData == null) ? new Hunt(this, targets) : new Hunt(this, targets, customStartScreenData); this.hunt.saveToPreferences(this); } customAssetCache = new CustomAssetCache(this); customAssetCache.downloadCustomAssets(customAssetUrlMap, new AssetFetcherCallback() { @Override public void requestComplete() { Log.i(TAG, "custom assets downloaded successfully"); } @Override public void requestFailed(Integer responseCode, Exception e) { Log.e(TAG, "Failed to download the custom assets."); } }); // After we have all our data from ProximityKit, we need to download the images and cache them // for display in the app. We do this every time, so that the app can update the images after // later, and have users get the update if they restart the app. This takes time, so if you // don't want to do this, then only execute this code if validateRequiredImagesPresent() // returns false. remoteAssetCache = new RemoteAssetCache(this); remoteAssetCache.downloadAssets(urlMap, new AssetFetcherCallback() { @Override public void requestComplete() { dependencyLoadFinished(); } @Override public void requestFailed(Integer responseCode, Exception e) { dependencyLoadFinished(); } }); } @Override public void didFailSync(Exception e) { Log.w(TAG, "proximityKit didFailSync"); if (ignoreSync) { Log.d(TAG, "ignoring sync"); return; } ignoreSync = true; // called when ProximityKit data are requested from the server, but the request fails if (loadingActivity != null && loadingActivity.isValidatingCode()) { Log.w(TAG, "proximityKit didFailSync due to " + e + " bad code entered?"); loadingActivity.codeValidationFailed(e); return; } Log.w(TAG, "proximityKit didFailSync due to " + e + " We may be offline."); hunt = Hunt.loadFromPreferences(this); this.dependencyLoadFinished(); } // This method is called when we have tried to download all dependencies (ProximityKit data and // all target images.) This may or may not have failed, so we check that everything loaded // properly. public void dependencyLoadFinished() { Log.d(TAG, "all dependencies loaded"); if (ProximityKitManager.getInstanceForApplication(this).getKit() == null || hunt.getTargetList().size() == 0) { dependencyLoadingFailed("Network error", "Can't access scavenger hunt data. Please verify your network connection and try again."); return; } if (validateRequiredImagesPresent()) { // Yes, we have everything we need to start up. this.hunt.start(); Intent i; if (hunt.hasCustomStartScreen()) { i = new Intent(this, InstructionActivity.class); } else { i = new Intent(this, TargetCollectionActivity.class); } i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); startActivity(i); this.loadingActivity.finish(); // do this so that if we hit back, the loading activity won't show up again return; } else { dependencyLoadingFailed("Network error", "Can't download images. Please verify your network connection and try again."); return; } } public Hunt getHunt() { return hunt; } public TargetCollectionActivity getCollectionActivity() { return collectionActivity; } public void setCollectionActivity(TargetCollectionActivity collectionActivity) { this.collectionActivity = collectionActivity; } public TargetItemActivity getItemActivity() { return itemActivity; } public void setItemActivity(TargetItemActivity itemActivity) { this.itemActivity = itemActivity; } public boolean isCodeNeeded() { return this.codeNeeded; } public RemoteAssetCache getRemoteAssetCache() { return remoteAssetCache; } public CustomAssetCache getCustomAssetCache() { return customAssetCache; } // Checks to see that one found and one not found image has been downloaded for each target public boolean validateRequiredImagesPresent() { Log.d(TAG, "Validating required images are present"); boolean missing = false; for (TargetItem target : hunt.getTargetList()) { if (remoteAssetCache.getImageByName("target" + target.getId()) == null) { missing = true; } if (remoteAssetCache.getImageByName("target" + target.getId() + "_found") == null) { missing = true; } } return !missing; } /* Sends a notification to the user when a scavenger hunt beacon is nearby. */ private void sendNotification() { try { NotificationCompat.Builder builder = new NotificationCompat.Builder(this) .setContentTitle(getString(R.string.sh_notification_title)) .setContentText(getString(R.string.sh_notification_text)).setVibrate(VIBRATOR_PATTERN) .setSmallIcon(R.drawable.sh_notification_icon).setAutoCancel(true); TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); Intent notificationIntent = new Intent(this, TargetCollectionActivity.class); notificationIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); stackBuilder.addNextIntent(notificationIntent); PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); builder.setContentIntent(resultPendingIntent); NotificationManager notificationManager = (NotificationManager) this .getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(1, builder.build()); } catch (Exception e) { e.printStackTrace(); } } public void cancelAllNotifications() { try { NotificationManager notificationManager = (NotificationManager) this .getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancelAll(); } catch (Exception e) { e.printStackTrace(); } } /* Converts a target image URL to a variant needed for this platform. The variant URL might add a suffix indicating a larger size for the image. A suffix is also added if the target is found. So a URL like this: http://mysite.com/target1.jpg might become: http://mysite.com/target1_found_624.jpg */ private String variantTargetImageUrlForBaseUrlString(String baseUrlString, boolean found) { int extensionIndex = baseUrlString.lastIndexOf("."); if (extensionIndex == -1) { return null; } Log.d(TAG, "Extension Index of " + baseUrlString + " is " + extensionIndex); String extension = baseUrlString.substring(extensionIndex); String prefix = baseUrlString.substring(0, extensionIndex); String suffix; if (found) { suffix = "_found"; } else { suffix = ""; } float screenWidthPixels = getResources().getDisplayMetrics().widthPixels; Log.d(TAG, "Screen width is " + screenWidthPixels + " and density is " + getResources().getDisplayMetrics().density); if (screenWidthPixels > 1040 * 2 * 1.2) { suffix += "_1040"; } else if (screenWidthPixels > 624 * 2 * 1.2) { suffix += "_624"; } else if (screenWidthPixels > 438 * 2 * 1.2) { suffix += "_438"; } else if (screenWidthPixels > 312 * 2 * 1.2) { suffix += "_312"; } else if (screenWidthPixels > 260 * 2 * 1.2) { suffix += "_260"; } return prefix + suffix + extension; } public static boolean isApplicationSentToBackground(final Context context) { if (context != null) { ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); List<ActivityManager.RunningTaskInfo> tasks = am.getRunningTasks(1); if (!tasks.isEmpty()) { ComponentName topActivity = tasks.get(0).topActivity; if (!topActivity.getPackageName().equals(context.getPackageName())) { return true; } } } return false; } }