/* * Copyright 2015 Lafayette College * * This file is part of OpenCVTour. * * OpenCVTour is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * OpenCVTour is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenCVTour. If not, see <http://www.gnu.org/licenses/>. */ package alicrow.opencvtour; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.location.Location; import android.media.MediaPlayer; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import com.eyeem.recyclerviewtools.adapter.WrapAdapter; import org.opencv.android.BaseLoaderCallback; import org.opencv.android.LoaderCallbackInterface; import org.opencv.android.OpenCVLoader; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * Activity to follow a Tour. */ public class FollowTourActivity extends AppCompatActivity implements View.OnClickListener { private static final String TAG = "FollowTourActivity"; private LocationService.ServiceConnection _connection; private boolean _service_is_bound = false; private ArrayList<Integer> _visited_item_ids; private ArrayList<TourItem> _remaining_items; private TourItem _current_item; private Location _current_location; private MediaPlayer _player = null; private TourItemAdapter _adapter; class TourItemAdapter extends RecyclerView.Adapter<TourItemAdapter.ViewHolder> { final List<TourItem> _tour_items; public class ViewHolder extends RecyclerView.ViewHolder { public final TextView _item_name; public final TextView _item_directions; public ViewHolder(RelativeLayout v) { super(v); _item_name = (TextView) v.findViewById(R.id.item_name); _item_directions = (TextView) v.findViewById(R.id.item_directions); } } public TourItemAdapter(List<TourItem> tour_items) { _tour_items = tour_items; } @Override public TourItemAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.follow_tour_item_line, parent, false); return new ViewHolder((RelativeLayout) v); } @Override public void onBindViewHolder(ViewHolder holder, int position) { TourItem item = _tour_items.get(position); holder._item_name.setText(item.getName()); holder._item_directions.setText(item.getDirections()); if(item.getDirections().equals("")) holder._item_directions.setVisibility(View.GONE); else holder._item_directions.setVisibility(View.VISIBLE); } @Override public int getItemCount() { return _tour_items.size(); } } @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(getIntent().getData() != null) { /// FollowTourActivity was launched directly, with a Tour archive. Must initialize OpenCV and load the Tour before we can set up the Activity. /// This happens when another app calls our app to open a tour archive. OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_0_0, this, new BaseLoaderCallback(this) { @Override public void onManagerConnected(int status) { switch (status) { case LoaderCallbackInterface.SUCCESS: { Log.i(TAG, "OpenCV loaded successfully"); /// Load the tour file Uri data = getIntent().getData(); String scheme = data.getScheme(); File extracted_folder = null; if(ContentResolver.SCHEME_CONTENT.equals(scheme)) { /// content uri try { /// Try to retrieve the filename so we know what to name our extracted folder. String folder_name; Cursor cursor = getContentResolver().query(data, new String[]{"_display_name"}, null, null, null); cursor.moveToFirst(); int nameIndex = cursor.getColumnIndex("_display_name"); if (nameIndex >= 0) { folder_name = cursor.getString(nameIndex); } else { folder_name = "imported-folder.zip.tour"; } cursor.close(); folder_name = folder_name.substring(0, folder_name.length() - ".zip.tour".length()); extracted_folder = new File(Tour.getImportedToursDirectory(FollowTourActivity.this), folder_name); Utilities.extractFolder(getContentResolver().openInputStream(data), extracted_folder.getPath()); } catch(FileNotFoundException ex) { Log.e(TAG, ex.getMessage()); /// Import failed. Exit the activity. FollowTourActivity.this.finish(); } } else { /// regular file uri String filepath = data.getPath(); String folder_name = data.getLastPathSegment(); folder_name = folder_name.substring(0, folder_name.length() - ".zip.tour".length()); extracted_folder = new File(Tour.getImportedToursDirectory(FollowTourActivity.this), folder_name); Utilities.extractFolder(filepath, extracted_folder.getPath()); } Tour.setSelectedTour(new Tour(new File(extracted_folder, "tour.yaml"))); load(savedInstanceState); break; } default: { super.onManagerConnected(status); break; } } } }); } else load(savedInstanceState); } /// Called from onCreate once OpenCV is initialized and the current tour is loaded. private void load(Bundle savedInstanceState) { setContentView(R.layout.activity_follow_tour); RecyclerView recycler_view = (RecyclerView) findViewById(R.id.remaining_items_list); recycler_view.setLayoutManager(new LinearLayoutManager(this)); findViewById(R.id.fab).setOnClickListener(this); findViewById(R.id.exit_button).setOnClickListener(this); findViewById(R.id.restart_button).setOnClickListener(this); if(Tour.getCurrentTour().getGpsEnabled()) bindLocationService(); _visited_item_ids = new ArrayList<>(); if(savedInstanceState != null) { if(savedInstanceState.containsKey("_current_location")) { _current_location = savedInstanceState.getParcelable("_current_location"); } if(savedInstanceState.containsKey("_visited_item_ids")) { _visited_item_ids = savedInstanceState.getIntegerArrayList("_visited_item_ids"); } } _remaining_items = new ArrayList<>(); for(TourItem item : Tour.getCurrentTour().getTourItems()) { if(!_visited_item_ids.contains((int) item.getId())) _remaining_items.add(item); } /// Add footer so the floating action button doesn't cover up the list. _adapter = new TourItemAdapter(_remaining_items); WrapAdapter wrap_adapter = new WrapAdapter(_adapter); wrap_adapter.addFooter(getLayoutInflater().inflate(R.layout.empty_list_footer, recycler_view, false)); recycler_view.setAdapter(wrap_adapter); if(savedInstanceState != null && savedInstanceState.containsKey("current_item_id")) { setCurrentItem(Tour.getCurrentTour().getTourItem(savedInstanceState.getLong("current_item_id"))); } updateDisplay(); } @Override public void onClick(View v) { switch(v.getId()) { case R.id.fab: Utilities.takePicture(this, true); break; case R.id.exit_button: setResult(RESULT_OK); finish(); break; case R.id.restart_button: Intent intent = getIntent(); finish(); startActivity(intent); break; } } private void bindLocationService() { _connection = new LocationService.ServiceConnection(); Intent intent = new Intent(getApplicationContext(), LocationService.class); startService(intent); bindService(intent, _connection, Context.BIND_AUTO_CREATE); _service_is_bound = true; } private void unbindLocationService() { if (_service_is_bound) { unbindService(_connection); _service_is_bound = false; } } @Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroy called"); unbindLocationService(); /// Unless we're just reconfiguring the UI (due to screen rotation or similar), we should stop location updates, since we only need them when this activity is running. if(!isChangingConfigurations()) { stopService(new Intent(getApplicationContext(), LocationService.class)); } } @Override protected void onResume() { super.onResume(); Log.d(TAG, "onResume called"); if(_connection != null && _connection.getService() != null) _connection.getService().startLocationUpdates(); } @Override protected void onStop() { super.onStop(); Log.d(TAG, "onStop called"); /// Unless we're just reconfiguring the UI (due to screen rotation or similar), we should stop location updates, since we only need them when this activity is running. if(!isChangingConfigurations()) if(_connection != null && _connection.getService() != null) _connection.getService().stopLocationUpdates(); if(_player != null) stopAudio(); } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); Log.d(TAG, "onSaveInstanceState called"); /// Activity must be destroyed and recreated when the screen orientation changes, so we need to save some things so the activity can be recreated if needed. if(_connection != null && _connection.getService() != null) _current_location = _connection.getService().getCurrentLocation(); if(_current_location != null) outState.putParcelable("_current_location", _current_location); else Log.d(TAG, "_current_location is null"); outState.putIntegerArrayList("_visited_item_ids", _visited_item_ids); if(_current_item != null) outState.putLong("current_item_id", _current_item.getId()); } /// Identify the item the user took a picture of, and progress accordingly private void identifyItem() { Tour current_tour = Tour.getCurrentTour(); List<TourItem> filtered_items = current_tour.getTourItems(); if(current_tour.getGpsEnabled()) { /// Filter out distant tour items if(_connection.getService() != null) _current_location = _connection.getService().getCurrentLocation(); if (_current_location == null) { Log.e(TAG, "got null current location"); Toast.makeText(this, "Couldn't determine location. Ensure location is enabled on your device, and/or wait a few seconds and try again.", Toast.LENGTH_LONG).show(); return; } filtered_items = filterDistantTourItems(filtered_items, _current_location, current_tour.getItemRange()); } else _current_location = null; List<Long> filtered_item_ids = new ArrayList<>(); for(TourItem item : filtered_items) { filtered_item_ids.add(item.getId()); } /// photo_filepath is the filepath we use for temporary images. If this changes, it needs to be changed in Utilities.takePicture() as well. String photo_filepath = new File(getExternalCacheDir(), "temp" + ".jpg").getPath(); TourItem detected_item = current_tour.getTourItem(current_tour.getDetector().identifyObject(photo_filepath, filtered_item_ids)); if(detected_item != null) { Log.i(TAG, "detected item named " + detected_item.getName()); if(_current_location != null) Log.i(TAG, "distance is " + _current_location.distanceTo(detected_item.getLocation()) + " meters"); } else { Log.i(TAG, "got null TourItem"); Toast.makeText(this, "No tour item detected.", Toast.LENGTH_SHORT).show(); } if(detected_item != null && (!current_tour.getEnforceOrder() || getNextTourItem() == detected_item)) { /// Detected a tour item. Update accordingly setCurrentItem(detected_item); if(detected_item.hasAudioFile()) playAudio(); } } /// Plays the audio file associated with the current tour item private void playAudio() { _player = new MediaPlayer(); try { _player.setDataSource(_current_item.getAudioFilepath()); _player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { mp.start(); } }); _player.prepareAsync(); } catch (IOException e) { Log.e(TAG, "prepare() failed"); } } private void stopAudio() { _player.release(); _player = null; } /** * Filters out distant TourItems * @param items unfiltered list of tour items * @param current_location the phone's current location * @param threshold maximum distance, in meters, a tour item can be from us to pass the filter * @return a filtered list of tour items within threshold meters of current_location */ private static List<TourItem> filterDistantTourItems(List<TourItem> items, Location current_location, double threshold) { List<TourItem> nearby_items = new ArrayList<>(); for(TourItem item : items) { /// if the item is close enough, or doesn't have a location set, we add it to the list if(item.getLocation() == null || current_location.distanceTo(item.getLocation()) < threshold) { nearby_items.add(item); } if(item.getLocation() != null) Log.d(TAG, item.getName() + " is " + current_location.distanceTo(item.getLocation()) + " meters away"); } return nearby_items; } /// Set the current tour item and perform necessary updates private void setCurrentItem(TourItem item) { _current_item = item; findViewById(R.id.current_item_container).setVisibility(View.VISIBLE); ((TextView) findViewById(R.id.current_tour_item_name)).setText(item.getName()); ((TextView) findViewById(R.id.current_tour_item_description)).setText(item.getDescription()); // mark that item as visited if(!_visited_item_ids.contains((int) item.getId())) _visited_item_ids.add((int) item.getId()); _remaining_items.remove(item); _adapter.notifyDataSetChanged(); updateDisplay(); } /// updates the UI when we reach a new tour item. private void updateDisplay() { /// There are three different sets of UI elements we can display: if the tour items have to be visited in order, we just display the next item in the list; if the tour can be followed in any order, we display a list of the remaining items; if all items have already been visited, then we display "tour complete" along with buttons to restart or exit. /// We need to figure out which elements to show, and set the unused elements to "GONE" so they won't show up or affect the layout. if(Tour.getCurrentTour().getEnforceOrder()) { findViewById(R.id.remaining_items_header).setVisibility(View.GONE); findViewById(R.id.remaining_items_list).setVisibility(View.GONE); findViewById(R.id.directions).setVisibility(View.VISIBLE); if(getNextTourItem() == null) { /// End of tour. Hide next_item_container and the floating action button, and display tour_complete_container findViewById(R.id.next_item_container).setVisibility(View.GONE); findViewById(R.id.fab).setVisibility(View.GONE); findViewById(R.id.tour_complete_container).setVisibility(View.VISIBLE); } else { if(!getNextTourItem().getDirections().equals("")) { ((TextView) findViewById(R.id.directions)).setText(getNextTourItem().getDirections()); } else { findViewById(R.id.directions).setVisibility(View.INVISIBLE); } ((TextView) findViewById(R.id.next_item_name)).setText(getNextTourItem().getName()); } } else { findViewById(R.id.next_item_container).setVisibility(View.GONE); if(_remaining_items.isEmpty()) { /// End of tour. Hide the list of remaining items and the floating action button, and display tour_complete_container findViewById(R.id.remaining_items_header).setVisibility(View.GONE); findViewById(R.id.remaining_items_list).setVisibility(View.GONE); findViewById(R.id.fab).setVisibility(View.GONE); findViewById(R.id.tour_complete_container).setVisibility(View.VISIBLE); } } } /// returns the tour item that comes after the current item, or null if the tour is over private TourItem getNextTourItem() { int index; if(_current_item == null) index = 0; else index = Tour.getCurrentTour().getTourItems().indexOf(_current_item) + 1; if(Tour.getCurrentTour().getTourItems().size() == index) return null; /// Tour is finished return Tour.getCurrentTour().getTourItems().get(index); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if(resultCode == Activity.RESULT_OK && requestCode == Utilities.REQUEST_IMAGE_CAPTURE) { identifyItem(); } } }