org.physical_web.physicalweb.NearbyBeaconsFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.physical_web.physicalweb.NearbyBeaconsFragment.java

Source

/*
 * 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 org.physical_web.physicalweb;

import org.physical_web.collection.PhysicalWebCollection;
import org.physical_web.collection.PwPair;
import org.physical_web.collection.PwsResult;
import org.physical_web.collection.UrlDevice;

import android.annotation.SuppressLint;
import android.app.ListFragment;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.PorterDuff;
import android.graphics.drawable.AnimationDrawable;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.support.v4.content.ContextCompat;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * This class shows the ui list for all
 * detected nearby beacons.
 * It also listens for tap events
 * on items within the list.
 * Tapped list items then launch
 * the browser and point that browser
 * to the given list items url.
 */
public class NearbyBeaconsFragment extends ListFragment
        implements UrlDeviceDiscoveryService.UrlDeviceDiscoveryListener, SwipeRefreshWidget.OnRefreshListener {

    private static final String TAG = NearbyBeaconsFragment.class.getSimpleName();
    private static final long FIRST_SCAN_TIME_MILLIS = TimeUnit.SECONDS.toMillis(2);
    private static final long SECOND_SCAN_TIME_MILLIS = TimeUnit.SECONDS.toMillis(5);
    private static final long THIRD_SCAN_TIME_MILLIS = TimeUnit.SECONDS.toMillis(10);
    private List<String> mGroupIdQueue;
    private PhysicalWebCollection mPwCollection = null;
    private TextView mScanningAnimationTextView;
    private AnimationDrawable mScanningAnimationDrawable;
    private Handler mHandler;
    private NearbyBeaconsAdapter mNearbyDeviceAdapter;
    private SwipeRefreshWidget mSwipeRefreshWidget;
    private boolean mSecondScanComplete;
    private boolean mFirstTime;
    private DiscoveryServiceConnection mDiscoveryServiceConnection;
    private boolean mMissedEmptyGroupIdQueue = false;
    private SwipeDismissListViewTouchListener mTouchListener;
    private WifiDirectConnect mWifiDirectConnect;
    private BluetoothSite mBluetoothSite;

    // The display of gathered urls happens as follows
    // 0. Begin scan
    // 1. Sort and show all urls (mFirstScanTimeout)
    // 2. Sort and show all new urls beneath the first set (mSecondScanTimeout)
    // 3. Show each new url at bottom of list as it comes in
    // 4. Stop scanning (mThirdScanTimeout)

    // Run when the FIRST_SCAN_MILLIS has elapsed.
    private Runnable mFirstScanTimeout = new Runnable() {
        @Override
        public void run() {
            Log.d(TAG, "running first scan timeout");
            if (!mGroupIdQueue.isEmpty()) {
                emptyGroupIdQueue();
                mSwipeRefreshWidget.setRefreshing(false);
                mScanningAnimationDrawable.stop();
            }
        }
    };

    // Run when the SECOND_SCAN_MILLIS has elapsed.
    private Runnable mSecondScanTimeout = new Runnable() {
        @Override
        public void run() {
            Log.d(TAG, "running second scan timeout");
            emptyGroupIdQueue();
            mSecondScanComplete = true;
            mSwipeRefreshWidget.setRefreshing(false);
            mScanningAnimationDrawable.stop();
            if (mNearbyDeviceAdapter.getCount() == 0) {
                int tintColor = ContextCompat.getColor(getActivity(), R.color.physical_web_logo);
                mScanningAnimationDrawable.setColorFilter(tintColor, PorterDuff.Mode.SRC_IN);
                mScanningAnimationTextView.setText(R.string.empty_nearby_beacons_list_text_no_results);
            }
        }
    };

    // Run when the THIRD_SCAN_MILLIS has elapsed.
    private Runnable mThirdScanTimeout = new Runnable() {
        @Override
        public void run() {
            Log.d(TAG, "running third scan timeout");
            mDiscoveryServiceConnection.disconnect();
        }
    };

    /**
     * The connection to the service that discovers urls.
     */
    private class DiscoveryServiceConnection implements ServiceConnection {
        private UrlDeviceDiscoveryService mDiscoveryService;
        private boolean mRequestCachedUrlDevices;

        @Override
        public synchronized void onServiceConnected(ComponentName className, IBinder service) {
            // Get the service
            UrlDeviceDiscoveryService.LocalBinder localBinder = (UrlDeviceDiscoveryService.LocalBinder) service;
            mDiscoveryService = localBinder.getServiceInstance();

            // Start the scanning display
            mDiscoveryService.addCallback(NearbyBeaconsFragment.this);
            if (!mRequestCachedUrlDevices) {
                mDiscoveryService.restartScan();
            }
            mPwCollection = mDiscoveryService.getPwCollection();
            // Make sure cached results get placed in the mGroupIdQueue.
            onUrlDeviceDiscoveryUpdate();
            startScanningDisplay(mDiscoveryService.getScanStartTime(), mDiscoveryService.hasResults());
        }

        @Override
        public synchronized void onServiceDisconnected(ComponentName className) {
            // onServiceDisconnected gets called when the connection is unintentionally disconnected,
            // which should never happen for us since this is a local service
            mDiscoveryService = null;
        }

        public synchronized void connect(boolean requestCachedUrlDevices) {
            if (mDiscoveryService != null) {
                return;
            }

            mRequestCachedUrlDevices = requestCachedUrlDevices;
            Intent intent = new Intent(getActivity(), UrlDeviceDiscoveryService.class);
            getActivity().startService(intent);
            getActivity().bindService(intent, this, Context.BIND_AUTO_CREATE);
        }

        public synchronized void disconnect() {
            if (mDiscoveryService == null) {
                return;
            }

            mDiscoveryService.removeCallback(NearbyBeaconsFragment.this);
            mDiscoveryService = null;
            getActivity().unbindService(this);
            stopScanningDisplay();
        }
    }

    private void initialize(View rootView) {
        setHasOptionsMenu(true);
        mGroupIdQueue = new ArrayList<>();
        mHandler = new Handler();

        mSwipeRefreshWidget = (SwipeRefreshWidget) rootView.findViewById(R.id.swipe_refresh_widget);
        mSwipeRefreshWidget.setColorSchemeResources(R.color.swipe_refresh_widget_first_color,
                R.color.swipe_refresh_widget_second_color);
        mSwipeRefreshWidget.setOnRefreshListener(this);

        getActivity().getActionBar().setTitle(R.string.title_nearby_beacons);
        mNearbyDeviceAdapter = new NearbyBeaconsAdapter();
        setListAdapter(mNearbyDeviceAdapter);
        //Get the top drawable
        mScanningAnimationTextView = (TextView) rootView.findViewById(android.R.id.empty);
        mScanningAnimationDrawable = (AnimationDrawable) mScanningAnimationTextView.getCompoundDrawables()[1];
        ListView listView = (ListView) rootView.findViewById(android.R.id.list);
        mDiscoveryServiceConnection = new DiscoveryServiceConnection();
        mWifiDirectConnect = new WifiDirectConnect(getActivity());
        mBluetoothSite = new BluetoothSite(getActivity());
        mTouchListener = new SwipeDismissListViewTouchListener(listView,
                new SwipeDismissListViewTouchListener.DismissCallbacks() {
                    @Override
                    public boolean canDismiss(int position) {
                        return true;
                    }

                    @Override
                    public void onDismiss(ListView listView, int position) {
                        Utils.addBlocked(mNearbyDeviceAdapter.getItem(position));
                        Utils.saveBlocked(getActivity());
                        if (mMissedEmptyGroupIdQueue) {
                            mMissedEmptyGroupIdQueue = false;
                            emptyGroupIdQueue();
                        }
                    }
                });
        listView.setOnTouchListener(mTouchListener);

        // Setting this scroll listener is required to ensure that during ListView scrolling,
        // we don't look for swipes.
        listView.setOnScrollListener(mTouchListener.makeScrollListener());
        Utils.restoreFavorites(getActivity());
        Utils.restoreBlocked(getActivity());
    }

    @Override
    public View onCreateView(LayoutInflater layoutInflater, ViewGroup container, Bundle savedInstanceState) {
        mFirstTime = true;
        View rootView = layoutInflater.inflate(R.layout.fragment_nearby_beacons, container, false);
        initialize(rootView);
        return rootView;
    }

    @Override
    public void onResume() {
        super.onResume();
        getActivity().getActionBar().setTitle(R.string.title_nearby_beacons);
        getActivity().getActionBar().setDisplayHomeAsUpEnabled(false);
        if (mFirstTime && !PermissionCheck.getInstance().isCheckingPermissions()) {
            restartScan();
        }
        mFirstTime = false;
    }

    public void restartScan() {
        if (mDiscoveryServiceConnection != null) {
            mDiscoveryServiceConnection.connect(true);
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        mDiscoveryServiceConnection.disconnect();
    }

    @Override
    public void onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);
        menu.findItem(R.id.action_about).setVisible(true);
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        PwPair item = mNearbyDeviceAdapter.getItem(position);
        // If we are scanning or user clicked on folder
        if (mScanningAnimationDrawable.isRunning() || isFolderItem(item)) {
            // Don't respond to touch events
            return;
        }
        // Get the url for the given item
        PwsResult pwsResult = item.getPwsResult();
        if (Utils.isWifiDirectDevice(item.getUrlDevice())) {
            // Initiate WifiDirect Connection request to device
            mWifiDirectConnect.connect(item.getUrlDevice(), pwsResult.getTitle());
        } else if (Utils.isFatBeaconDevice(item.getUrlDevice())) {
            if (!mBluetoothSite.isRunning()) {
                mBluetoothSite.connect(pwsResult.getSiteUrl(), pwsResult.getTitle());
            }
        } else {
            Intent intent = Utils.createNavigateToUrlIntent(pwsResult);
            startActivity(intent);
        }
    }

    @Override
    public void onUrlDeviceDiscoveryUpdate() {
        // Since this callback is given on a background thread and we want
        // to update the list adapter (which can only be done on the UI thread)
        // we have to interact with the adapter on the UI thread.
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                if (SwipeDismissListViewTouchListener.isLocked()) {
                    return;
                }
                for (PwPair pwPair : mPwCollection
                        .getGroupedPwPairsSortedByRank(new Utils.PwPairRelevanceComparator())) {
                    String groupId = Utils.getGroupId(pwPair.getPwsResult());
                    Log.d(TAG, "groupid to add " + groupId);
                    if (mNearbyDeviceAdapter.containsGroupId(groupId)) {
                        mNearbyDeviceAdapter.updateItem(pwPair);
                    } else if (!mGroupIdQueue.contains(groupId) && !Utils.isBlocked(pwPair)) {
                        mGroupIdQueue.add(groupId);
                        if (mSecondScanComplete) {
                            // If we've already waited for the second scan timeout,
                            // go ahead and put the item in the listview.
                            emptyGroupIdQueue();
                        }
                    }
                }
                mNearbyDeviceAdapter.notifyDataSetChanged();
            }
        });
    }

    private void stopScanningDisplay() {
        // Cancel the scan timeout callback if still active or else it may fire later.
        mHandler.removeCallbacks(mFirstScanTimeout);
        mHandler.removeCallbacks(mSecondScanTimeout);
        mHandler.removeCallbacks(mThirdScanTimeout);

        // Change the display appropriately
        mSwipeRefreshWidget.setRefreshing(false);
        mScanningAnimationDrawable.stop();
    }

    private void startScanningDisplay(long scanStartTime, boolean hasResults) {
        // Start the scanning animation only if we don't haven't already been scanning
        // for long enough
        Log.d(TAG, "startScanningDisplay " + scanStartTime + " " + hasResults);
        long elapsedMillis = new Date().getTime() - scanStartTime;
        if (elapsedMillis < FIRST_SCAN_TIME_MILLIS || (elapsedMillis < SECOND_SCAN_TIME_MILLIS && !hasResults)) {
            mNearbyDeviceAdapter.clear();
            mScanningAnimationDrawable.setColorFilter(null);
            mScanningAnimationTextView.setText(R.string.empty_nearby_beacons_list_text);
            mScanningAnimationDrawable.start();
        } else {
            mSwipeRefreshWidget.setRefreshing(false);
            mScanningAnimationDrawable.stop();
        }

        // Schedule the timeouts
        mSecondScanComplete = false;
        long firstDelay = Math.max(FIRST_SCAN_TIME_MILLIS - elapsedMillis, 0);
        long secondDelay = Math.max(SECOND_SCAN_TIME_MILLIS - elapsedMillis, 0);
        long thirdDelay = Math.max(THIRD_SCAN_TIME_MILLIS - elapsedMillis, 0);
        mHandler.postDelayed(mFirstScanTimeout, firstDelay);
        mHandler.postDelayed(mSecondScanTimeout, secondDelay);
        mHandler.postDelayed(mThirdScanTimeout, thirdDelay);
    }

    @Override
    public void onRefresh() {
        // Clear any stored url data
        mGroupIdQueue.clear();
        mNearbyDeviceAdapter.clear();

        // Reconnect to the service
        mDiscoveryServiceConnection.disconnect();
        mSwipeRefreshWidget.setRefreshing(true);
        mDiscoveryServiceConnection.connect(false);
    }

    private void emptyGroupIdQueue() {
        if (SwipeDismissListViewTouchListener.isLocked()) {
            mMissedEmptyGroupIdQueue = true;
            return;
        }

        List<PwPair> pwPairs = new ArrayList<>();

        for (String groupId : mGroupIdQueue) {
            Log.d(TAG, "groupid " + groupId);
            pwPairs.add(Utils.getTopRankedPwPairByGroupId(mPwCollection, groupId));
        }
        Collections.sort(pwPairs, new Utils.PwPairRelevanceComparator());
        for (PwPair pwPair : pwPairs) {
            mNearbyDeviceAdapter.addItem(pwPair);
        }
        mGroupIdQueue.clear();
        mNearbyDeviceAdapter.notifyDataSetChanged();
    }

    private static boolean isFolderItem(PwPair item) {
        return item.getUrlDevice() == null && item.getPwsResult().getSiteUrl() == null;
    }

    // Adapter for holding beacons found through scanning.
    private class NearbyBeaconsAdapter extends BaseAdapter {
        private List<PwPair> mPwPairs;
        private int mNumberOfHideableResults;

        NearbyBeaconsAdapter() {
            mPwPairs = new ArrayList<>();
            mNumberOfHideableResults = 0;
        }

        public void addItem(PwPair pwPair) {
            // If isResolvableDevice place in the folder at the bottom
            // of the list (making the folder if it didn't already exist)
            // Otherwise place in the bottom of the non-folder list
            if (Utils.isResolvableDevice(pwPair.getUrlDevice())) {
                mPwPairs.add(mPwPairs.size() - mNumberOfHideableResults, pwPair);
                return;
            }
            if (mNumberOfHideableResults == 0) {
                mPwPairs.add(new PwPair(null, new PwsResult(null, null)));
                mNumberOfHideableResults++;
            }
            mNumberOfHideableResults++;
            mPwPairs.add(pwPair);
        }

        public void updateItem(PwPair pwPair) {
            String groupId = Utils.getGroupId(pwPair.getPwsResult());
            for (int i = 0; i < mPwPairs.size(); ++i) {
                if (isFolderItem(mPwPairs.get(i))) {
                    continue;
                }
                if (Utils.getGroupId(mPwPairs.get(i).getPwsResult()).equals(groupId)) {
                    mPwPairs.set(i, pwPair);
                    return;
                }
            }
            throw new RuntimeException("Cannot find PwPair with group " + groupId);
        }

        public boolean containsGroupId(String groupId) {
            for (PwPair pwPair : mPwPairs) {
                if (isFolderItem(pwPair)) {
                    continue;
                }
                if (Utils.getGroupId(pwPair.getPwsResult()).equals(groupId)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public int getCount() {
            return mPwPairs.size();
        }

        @Override
        public PwPair getItem(int i) {
            return mPwPairs.get(i);
        }

        @Override
        public long getItemId(int i) {
            return i;
        }

        private void setText(View view, int textViewId, String text) {
            ((TextView) view.findViewById(textViewId)).setText(text);
        }

        @SuppressLint("InflateParams")
        @Override
        public View getView(int i, View view, ViewGroup viewGroup) {
            // Display the pwsResult.
            PwPair pwPair = getItem(i);
            PwsResult pwsResult = pwPair.getPwsResult();
            if (isFolderItem(pwPair)) {
                view = getActivity().getLayoutInflater().inflate(R.layout.folder_item_nearby_beacon, viewGroup,
                        false);
                WifiManager wifiManager = (WifiManager) getActivity().getSystemService(Context.WIFI_SERVICE);
                WifiInfo wifiInfo = wifiManager.getConnectionInfo();
                String ssid = wifiInfo.getSSID().trim();
                if (ssid.charAt(0) == '"' && ssid.charAt(ssid.length() - 1) == '"') {
                    setText(view, R.id.title, ssid.substring(1, ssid.length() - 1));
                } else {
                    setText(view, R.id.title, "Wireless Network");
                }
                return view;
            }
            view = getActivity().getLayoutInflater().inflate(R.layout.list_item_nearby_beacon, viewGroup, false);
            setText(view, R.id.title, pwsResult.getTitle());
            if (Utils.isFatBeaconDevice(pwPair.getUrlDevice())) {
                setText(view, R.id.url, getString(R.string.FatBeacon_URL) + pwsResult.getSiteUrl());
            } else {
                setText(view, R.id.url, pwsResult.getSiteUrl());
            }
            if (Utils.isResolvableDevice(pwPair.getUrlDevice())) {
                ((ImageView) view.findViewById(R.id.icon))
                        .setImageBitmap(Utils.getBitmapIcon(mPwCollection, pwsResult));
            } else {
                ((ImageView) view.findViewById(R.id.icon)).setImageResource(R.drawable.unresolved_result_icon);
            }
            setText(view, R.id.description, pwsResult.getDescription());
            final String siteUrl = pwsResult.getSiteUrl();

            if (Utils.isFavorite(siteUrl)) {
                ((Button) view.findViewById(R.id.star)).setBackgroundResource(R.drawable.ic_star_black_24dp);
                ((Button) view.findViewById(R.id.star)).setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Utils.toggleFavorite(siteUrl);
                        Utils.saveFavorites(getActivity());
                        ((Button) v).setBackgroundResource(R.drawable.ic_star_border_black_24dp);
                        notifyDataSetChanged();
                    }
                });
            } else {
                ((Button) view.findViewById(R.id.star)).setBackgroundResource(R.drawable.ic_star_border_black_24dp);
                ((Button) view.findViewById(R.id.star)).setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Utils.toggleFavorite(siteUrl);
                        Utils.saveFavorites(getActivity());
                        ((Button) v).setBackgroundResource(R.drawable.ic_star_black_24dp);
                        notifyDataSetChanged();
                    }
                });
            }

            if (Utils.isDebugViewEnabled(getActivity())) {
                // If we should show the ranging data
                updateDebugView(pwPair, view);
                view.findViewById(R.id.ranging_debug_container).setVisibility(View.VISIBLE);
                view.findViewById(R.id.metadata_debug_container).setVisibility(View.VISIBLE);
            } else {
                view.findViewById(R.id.ranging_debug_container).setVisibility(View.GONE);
                view.findViewById(R.id.metadata_debug_container).setVisibility(View.GONE);
            }
            return view;
        }

        private void updateDebugView(PwPair pwPair, View view) {
            // Ranging debug line
            UrlDevice urlDevice = pwPair.getUrlDevice();
            if (Utils.isBleUrlDevice(urlDevice)) {
                setText(view, R.id.ranging_debug_tx_power,
                        getString(R.string.ranging_debug_tx_power_prefix) + Utils.getTxPower(urlDevice));
                setText(view, R.id.ranging_debug_rssi,
                        getString(R.string.ranging_debug_rssi_prefix) + Utils.getSmoothedRssi(urlDevice));
                setText(view, R.id.ranging_debug_distance, getString(R.string.ranging_debug_distance_prefix)
                        + new DecimalFormat("##.##").format(Utils.getDistance(urlDevice)));
                setText(view, R.id.ranging_debug_region,
                        getString(R.string.ranging_debug_region_prefix) + Utils.getRegionString(urlDevice));
            } else {
                setText(view, R.id.ranging_debug_tx_power, "");
                setText(view, R.id.ranging_debug_rssi, "");
                setText(view, R.id.ranging_debug_distance, "");
                setText(view, R.id.ranging_debug_region, "");
            }

            // Metadata debug line
            setText(view, R.id.metadata_debug_scan_time, getString(R.string.metadata_debug_scan_time_prefix)
                    + new DecimalFormat("##.##s").format(Utils.getScanTimeMillis(urlDevice) / 1000.0));

            PwsResult pwsResult = pwPair.getPwsResult();
            setText(view, R.id.metadata_debug_rank,
                    getString(R.string.metadata_debug_rank_prefix) + new DecimalFormat("##.##").format(0)); // We currently do not use rank.
            if (Utils.isResolvableDevice(urlDevice)) {
                setText(view, R.id.metadata_debug_pws_trip_time,
                        getString(R.string.metadata_debug_pws_trip_time_prefix) + new DecimalFormat("##.##s")
                                .format(Utils.getPwsTripTimeMillis(pwsResult) / 1000.0));
            }
            setText(view, R.id.metadata_debug_groupid,
                    getString(R.string.metadata_debug_groupid_prefix) + Utils.getGroupId(pwsResult));
        }

        public void clear() {
            mPwPairs.clear();
            mNumberOfHideableResults = 0;
            notifyDataSetChanged();
        }
    }
}