/*
* 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();
setRefreshWidgetInvisible();
}
}
};
// 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;
setRefreshWidgetInvisible();
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() {
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(mGroupIdQueue.isEmpty() || !mSecondScanComplete) {
return;
}
// 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() {
emptyGroupIdQueue();
}
});
}
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
setRefreshWidgetInvisible();
}
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 {
setRefreshWidgetInvisible();
}
// 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;
}
private void setRefreshWidgetInvisible() {
mSwipeRefreshWidget.setRefreshing(false);
mScanningAnimationDrawable.stop();
mScanningAnimationTextView.setVisibility(View.INVISIBLE);
}
// 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();
}
}
}