com.radiusnetworks.museumguide.MuseumGuideApplication.java Source code

Java tutorial

Introduction

Here is the source code for com.radiusnetworks.museumguide.MuseumGuideApplication.java

Source

/**
 *
 * 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.museumguide;

import android.app.Activity;
import android.app.Application;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
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 android.widget.EditText;

import com.radiusnetworks.ibeacon.IBeacon;
import com.radiusnetworks.ibeacon.IBeaconData;
import com.radiusnetworks.ibeacon.Region;
import com.radiusnetworks.ibeacon.client.DataProviderException;
import com.radiusnetworks.proximity.ProximityKitManager;
import com.radiusnetworks.proximity.ProximityKitNotifier;
import com.radiusnetworks.proximity.ibeacon.powersave.BackgroundPowerSaver;
import com.radiusnetworks.proximity.licensing.PropertiesFile;
import com.radiusnetworks.proximity.model.KitIBeacon;
import com.radiusnetworks.museumguide.assets.AssetFetcherCallback;
import com.radiusnetworks.museumguide.assets.RemoteAssetCache;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Created by dyoung on 2/28/14.
 *
 * This is the central application class for the Museum Guide.  It is responsible for:
 * 1. Initializing ProxomityKit, which downloads all the iBeacons associated with the museum
 *    along with their configured item_id, image_url, html_url, and title values.  It then starts
 *    ranging and monitoring for these iBeacons, and will continue to do so even across
 *    a reboot.
 * 2. Downloads all the museum item images and html pages needed.
 * 3. Updates the LoadingActivity with the status of the above download state.
 * 4. Once loading completes, launches the MuseumItemsActivity which is the main screen for the
 *    museum, which shows a swipe view of all the museum items.
 * 5. Handles all ranging and monitoring callbacks for iBeacons.  When an iBeacon is ranged,
 *    this class checks to see if it matches a museum item and displays a notification on the
 *    MuseumItemsActivity that the next museum item is nearby.
 */

public class MuseumGuideApplication extends Application implements ProximityKitNotifier {

    private static final String TAG = "MuseumGuideApplication";
    private ProximityKitManager manager;
    @SuppressWarnings("unused")
    private BackgroundPowerSaver backgroundPowerSaver;
    private LoadingActivity loadingActivity = null;
    private MuseumItemsActivity museumItemsActivity;
    private RemoteAssetCache remoteAssetCache;
    private String loadingFailedTitle;
    private String loadingFailedMessage;
    private boolean codeNeeded;
    private VisibleMuseumItems visibleMuseumItems = new VisibleMuseumItems();
    private Museum museum = null;
    private Boolean displayedIntro = false;
    int startCount = 0;

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "Application onCreate.  Museum is " + museum);
        backgroundPowerSaver = new BackgroundPowerSaver(this);
        manager = ProximityKitManager.getInstanceForApplication(this);
        manager.setNotifier(this);
        manager.getIBeaconManager().setDebug(false);

        if (!new PropertiesFile().exists()) {
            SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
            String code = settings.getString("code", null);
            Log.d(TAG, "Code is needed");
            this.codeNeeded = true;
            return;
        } else {
            Log.d(TAG, "Code is not needed because we have a properties file");
            startPk(null);
        }
    }

    public void startPk(String code) {
        Log.d(TAG, "startPk called with code " + code);
        if (code != null) {
            manager.restart(code);
        } else {
            manager.start(); // This starts ranging and monitoring for iBeacons defined in ProximityKit\
        }
    }

    /**
     * switches to a different museum
     * @param activity
     */
    public void startOver(Activity activity) {
        if (!new PropertiesFile().exists()) {
            Log.d(TAG, "clearing shared preferences");
            SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
            String code = settings.getString("code", null);
            Boolean dontShowAgain = settings.getBoolean("dont_show_intro_again", false);
            SharedPreferences.Editor editor = settings.edit();
            setDisplayedIntro(false);
            editor.clear();
            editor.putString("code", code);
            editor.putBoolean("dont_show_intro_again", dontShowAgain);
            editor.commit();
            museum = null;
            remoteAssetCache = new RemoteAssetCache(this);
            remoteAssetCache.clear();

            this.codeNeeded = true;
        }

        Intent i = new Intent(activity, LoadingActivity.class);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        startActivity(i);
        if (this.museumItemsActivity != null) {
            this.museumItemsActivity.finish(); // do this so it won't show up again on back press
        }
    }

    public void setLoadingActivity(LoadingActivity activity) {
        this.loadingActivity = activity;
        if (this.loadingFailedTitle != null) {
            showFailedErrorMessage();
        }
    }

    // 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 activity 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;
    }

    private long lastNextMuseumItemRecalculationTime = 0l;

    @Override
    public void iBeaconDataUpdate(IBeacon iBeacon, IBeaconData iBeaconData, DataProviderException e) {
        // Called every second with data from ProximityKit when an iBeacon defined in ProximityKit is nearby
        Log.d(TAG, "iBeaconDataUpdate: " + iBeacon.getProximityUuid() + " " + iBeacon.getMajor() + " "
                + iBeacon.getMinor());
        String itemId = null;
        if (iBeaconData != null) {
            itemId = iBeaconData.get("item_id");
        }
        if (itemId == null) {
            Log.d(TAG, "The iBeacon I just saw is not part of the museum, according to ProximityKit");
            return;
        }
        if (museum == null) {
            // Museum has not been initialized from PK yet.  Ignoring all iBeacons
            return;
        }
        MuseumItem item = museum.getItemById(itemId);
        if (item == null) {
            Log.w(TAG, "The iBeacon I just saw has a item_id of " + itemId
                    + ", but it was not part of the museum when this app was started.");
            return;
        } else {
            visibleMuseumItems.detect(item, iBeacon.getAccuracy());
            long now = new java.util.Date().getTime();
            if (this.museumItemsActivity != null && now - lastNextMuseumItemRecalculationTime > 1000 /* 1 sec */) {
                this.museumItemsActivity.recalculateNextItem(visibleMuseumItems);
                lastNextMuseumItemRecalculationTime = now;
            }
        }

    }

    @Override
    public void didEnterRegion(Region region) {
        // Called when one of the iBeacons defined in ProximityKit first appears
        Log.d(TAG, "didEnterRegion");
        Log.d(TAG, "Sending notification.");
        sendNotification();
    }

    @Override
    public void didExitRegion(Region region) {
        // Called when one of the iBeacons defined in ProximityKit first disappears
        Log.d(TAG, "didExitRegion");
    }

    @Override
    public void didDetermineStateForRegion(int i, Region region) {
        // Called when one of the iBeacons 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());

        ArrayList<MuseumItem> museumItems = new ArrayList<MuseumItem>();
        Map<String, String> urlMap = new HashMap<String, String>();

        for (KitIBeacon iBeacon : manager.getKit().getIBeacons()) {
            String itemId = iBeacon.getAttributes().get("item_id");
            if (itemId != null) {
                String title = iBeacon.getAttributes().get("title");
                if (title == null) {
                    Log.e(TAG, "ERROR: No title specified in ProximityKit for item with item_id=" + itemId);
                }
                MuseumItem item = new MuseumItem(itemId, title);
                museumItems.add(item);
                String imageUrl = iBeacon.getAttributes().get("image_url");
                if (imageUrl == null) {
                    Log.e(TAG, "ERROR: No image_url specified in ProximityKit for item with item_id=" + itemId);
                }
                String htmlUrl = iBeacon.getAttributes().get("html_url");
                if (imageUrl == null) {
                    Log.e(TAG, "ERROR: No html_url specified in ProximityKit for item with item_id=" + itemId);
                }
                urlMap.put("item" + itemId, imageUrl);
                urlMap.put("item" + itemId + "_html", htmlUrl);
            }
        }

        if (museumItems.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"));
            }
        }

        // load the saved state of the hunt from the phone's persistent
        // storage
        museum = Museum.loadFromPreferences(this);
        boolean museumListChanged = museum.getItemList().size() != museumItems.size();
        for (MuseumItem item : museum.getItemList()) {
            boolean itemFound = false;
            for (MuseumItem itemFromPk : museumItems) {
                if (itemFromPk.getId().equals(item.getId()))
                    itemFound = true;
            }
            if (itemFound == false) {
                museumListChanged = true;
                Log.d(TAG, "Item with item_id=" + item.getId()
                        + " is no longer in PK.  Museum item list has changed.");
            }
        }
        if (museumListChanged) {
            Log.w(TAG, "the items in the museum have changed from what we have in the settings.  starting over");
            this.museum = new Museum(this, museumItems);
            this.museum.saveToPreferences(this);
        }

        // After we have all our data from ProximityKit, we need to download the images and html files
        // and cache them for display in the app.  We do this every time, so that the app can update the i
        // files, and have users get the update if they restart the app.
        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) {
        // 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.");
        museum = Museum.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 || museum == null
                || museum.getItemList().size() == 0) {
            dependencyLoadingFailed("Network error",
                    "Can't access museum data.  Please verify your network connection and try again.");
            return;
        }

        List<String> missingAssets = getMissingAssetList();
        if (missingAssets.size() == 0) {
            // Yes, we have everything we need to start up.  Let's start the image activity with the first exhibit item
            Intent i = new Intent(loadingActivity, MuseumItemsActivity.class);
            i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP
                    | Intent.FLAG_ACTIVITY_SINGLE_TOP);
            i.putExtra("item_id", museum.getItemList().get(0).getId());
            startActivity(i);
            this.loadingActivity.finish(); // do this so that if we hit back, the loading activity won't show up again
            return;
        } else {
            dependencyLoadingFailed("Error loading museum",
                    "Can't download images and/or html."
                            + "  Please verify your network connection and try again.  Missing items: "
                            + missingAssets.toString().replaceAll("(^\\[|\\]$)", ""));
            return;
        }
    }

    public VisibleMuseumItems getVisibleMuseumItems() {
        return visibleMuseumItems;
    }

    public Museum getMuseum() {
        return museum;
    }

    public MuseumItemsActivity getMuseumItemsActivity() {
        return museumItemsActivity;
    }

    public void setMuseumItemsActivity(MuseumItemsActivity museumItemsActivity) {
        this.museumItemsActivity = museumItemsActivity;
    }

    public boolean isCodeNeeded() {
        return this.codeNeeded;
    }

    public boolean hasDisplayedIntro() {
        return this.displayedIntro;
    }

    public void setDisplayedIntro(boolean value) {
        this.displayedIntro = value;
    }

    public RemoteAssetCache getRemoteAssetCache() {
        return remoteAssetCache;
    }

    public void setDontShowIntroAgain() {
        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
        SharedPreferences.Editor editor = settings.edit();
        editor.putBoolean("dont_show_intro_again", true);
        editor.commit();
    }

    public boolean getDontShowIntroAgain() {
        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(this);
        return settings.getBoolean("dont_show_intro_again", false);
    }

    // Checks to see that one found and one not found image has been downloaded for each target
    private List<String> getMissingAssetList() {
        Log.d(TAG, "Validating required assets are present");
        ArrayList<String> missingAssets = new ArrayList<String>();
        boolean missing = false;
        for (MuseumItem museumItem : museum.getItemList()) {
            if (remoteAssetCache.getImageByName("item" + museumItem.getId()) == null) {
                missingAssets.add("image for item " + museumItem.getId());
            }
            if (remoteAssetCache.getHtmlContentByName("item" + museumItem.getId() + "_html") == null) {
                missingAssets.add("html for item " + museumItem.getId());
            }
        }
        return missingAssets;
    }

    /*
     Sends a notification to the user when a beacon is nearby.
     */
    private void sendNotification() {
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this).setContentTitle("Museum Guide")
                .setContentText("An exhibit item is nearby.").setSmallIcon(R.drawable.launcher);

        TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
        stackBuilder.addNextIntent(new Intent(this, MuseumItemsActivity.class));
        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());
    }

}