diff --git a/src/main/java/de/dennisguse/opentracks/MarkerEditActivity.java b/src/main/java/de/dennisguse/opentracks/MarkerEditActivity.java index bc731a8bc..08fb32b18 100644 --- a/src/main/java/de/dennisguse/opentracks/MarkerEditActivity.java +++ b/src/main/java/de/dennisguse/opentracks/MarkerEditActivity.java @@ -16,18 +16,38 @@ package de.dennisguse.opentracks; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore; import android.util.Log; +import android.util.Pair; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.EditText; +import android.widget.ImageView; +import android.widget.Toast; + +import androidx.annotation.NonNull; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; import de.dennisguse.opentracks.content.data.Waypoint; import de.dennisguse.opentracks.content.provider.ContentProviderUtils; import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; +import de.dennisguse.opentracks.util.FileUtils; +import de.dennisguse.opentracks.util.IntentUtils; /** * An activity to add/edit a marker. @@ -39,16 +59,27 @@ public class MarkerEditActivity extends AbstractActivity { public static final String EXTRA_TRACK_ID = "track_id"; public static final String EXTRA_MARKER_ID = "marker_id"; + private static final int CAMERA_REQUEST_CODE = 5; + private static final int GALLERY_IMG_REQUEST_CODE = 7; + private static final String TAG = MarkerEditActivity.class.getSimpleName(); private long trackId; private long markerId; private TrackRecordingServiceConnection trackRecordingServiceConnection; private Waypoint waypoint; + private MenuItem insertPhotoMenuItem; + private MenuItem insertGalleryImgMenuItem; + + private Uri photoUri; + private boolean hasCamera; + // UI elements private EditText waypointName; private AutoCompleteTextView waypointMarkerType; private EditText waypointDescription; + private ImageView waypointPhoto; + private ImageView waypointDeletePhotoBtn; private Button done; @Override @@ -59,12 +90,27 @@ public class MarkerEditActivity extends AbstractActivity { markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L); trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null); + hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); + // Setup UI elements waypointName = findViewById(R.id.marker_edit_waypoint_name); waypointMarkerType = findViewById(R.id.marker_edit_waypoint_marker_type); ArrayAdapter adapter = ArrayAdapter.createFromResource(this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line); waypointMarkerType.setAdapter(adapter); waypointDescription = findViewById(R.id.marker_edit_waypoint_description); + waypointPhoto = findViewById(R.id.marker_edit_waypoint_photo); + + waypointDeletePhotoBtn = findViewById(R.id.marker_edit_waypoint_photo_delete); + waypointDeletePhotoBtn.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (waypoint != null && waypoint.hasPhoto()) + waypoint.setPhotoUrl(null); + waypointPhoto.setImageBitmap(null); + photoUri = null; + hideAndShowOptions(); + } + }); Button cancel = findViewById(R.id.marker_edit_cancel); cancel.setOnClickListener(new OnClickListener() { @@ -94,6 +140,64 @@ public class MarkerEditActivity extends AbstractActivity { return R.layout.marker_edit; } + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.marker_edit, menu); + + insertPhotoMenuItem = menu.findItem(R.id.marker_edit_insert_photo); + insertPhotoMenuItem.setVisible(hasCamera); + insertGalleryImgMenuItem = menu.findItem(R.id.marker_edit_insert_gallery_img); + hideAndShowOptions(); + + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.marker_edit_insert_photo: + createWaypointWithPicture(); + return true; + case R.id.marker_edit_insert_gallery_img: + createWaypointWithGalleryImage(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == CAMERA_REQUEST_CODE) { + if (resultCode == RESULT_CANCELED) { + Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show(); + return; + } else if (resultCode == RESULT_OK) { + setWaypointImageView(photoUri); + } + } else if (requestCode == GALLERY_IMG_REQUEST_CODE) { + if (resultCode == RESULT_CANCELED) { + Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show(); + return; + } else if (resultCode == RESULT_OK) { + Uri srcUri = data.getData(); + try (ParcelFileDescriptor parcelFd = getContentResolver().openFileDescriptor(srcUri, "r")) { + FileDescriptor srcFd = parcelFd.getFileDescriptor(); + File dstFile = new File(FileUtils.getImageUrl(this, getTrackId())); + FileUtils.copy(srcFd, dstFile); + + photoUri = FileUtils.getUriForFile(this, dstFile); + setWaypointImageView(photoUri); + } catch(Exception e) { + Log.e(TAG, e.getMessage()); + Toast.makeText(this, R.string.marker_add_canceled, Toast.LENGTH_LONG).show(); + return; + } + } + } + super.onActivityResult(requestCode, resultCode, data); + } + /** * Updates the UI based on the marker id. */ @@ -133,7 +237,62 @@ public class MarkerEditActivity extends AbstractActivity { waypointName.setText(waypoint.getName()); waypointMarkerType.setText(waypoint.getCategory()); waypointDescription.setText(waypoint.getDescription()); + if (waypoint.hasPhoto()) { + photoUri = waypoint.getPhotoURI(); + setWaypointImageView(photoUri); + } } + + hideAndShowOptions(); + } + + /** + * Checks and hide/shows all buttons/options about marker photo options. + * + * If a photo is set then one's options are shown, otherwise another ones are shown. + */ + private void hideAndShowOptions() { + boolean isPhotoSet = (waypoint != null && waypoint.hasPhoto()) || photoUri != null; + if (insertPhotoMenuItem != null && insertGalleryImgMenuItem != null) { + insertPhotoMenuItem.setVisible(isPhotoSet ? false : true); + insertGalleryImgMenuItem.setVisible(isPhotoSet ? false : true); + } + waypointDeletePhotoBtn.setVisibility(isPhotoSet ? View.VISIBLE : View.GONE); + } + + /** + * Returns the trackId. If trackId == -1 then get it from waypoint object. + */ + private long getTrackId() { + return trackId == -1 ? waypoint.getTrackId() : trackId; + } + + /** + * Sets the ImageView waypointPhoto with the uri photo. + * + * @param uri the uri photo. + */ + private void setWaypointImageView(Uri uri) { + try (ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r")) { + FileDescriptor fd = pfd.getFileDescriptor(); + Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd); + waypointPhoto.setImageBitmap(bitmap); + hideAndShowOptions(); + } catch(IOException e) { + Log.e(TAG, e.getMessage()); + Toast.makeText(this, R.string.marker_add_photo_canceled, Toast.LENGTH_LONG).show(); + } + } + + private void createWaypointWithPicture() { + Pair intentAndPhotoUri = IntentUtils.createTakePictureIntent(this, getTrackId()); + photoUri = intentAndPhotoUri.second; + startActivityForResult(intentAndPhotoUri.first, CAMERA_REQUEST_CODE); + } + + private void createWaypointWithGalleryImage() { + Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + startActivityForResult(intent, GALLERY_IMG_REQUEST_CODE); } /** @@ -144,7 +303,7 @@ public class MarkerEditActivity extends AbstractActivity { waypointName.getText().toString(), waypointMarkerType.getText().toString(), waypointDescription.getText().toString(), - null); + photoUri != null ? photoUri.toString() : null); } /** @@ -154,6 +313,7 @@ public class MarkerEditActivity extends AbstractActivity { waypoint.setName(waypointName.getText().toString()); waypoint.setCategory(waypointMarkerType.getText().toString()); waypoint.setDescription(waypointDescription.getText().toString()); + waypoint.setPhotoUrl(photoUri != null ? photoUri.toString() : null); new ContentProviderUtils(this).updateWaypoint(waypoint); } diff --git a/src/main/java/de/dennisguse/opentracks/TrackDetailActivity.java b/src/main/java/de/dennisguse/opentracks/TrackDetailActivity.java index 73dda882d..8b0b63420 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackDetailActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackDetailActivity.java @@ -16,25 +16,18 @@ package de.dennisguse.opentracks; -import android.Manifest; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.content.pm.PackageManager; -import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.provider.MediaStore; -import android.util.Pair; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.WindowManager; -import android.widget.Toast; import androidx.annotation.NonNull; -import androidx.core.app.ActivityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentPagerAdapter; import androidx.viewpager.widget.ViewPager; @@ -70,16 +63,8 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA private static final String TAG = TrackDetailActivity.class.getSimpleName(); private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key"; - private static final String PHOTO_URI_KEY = "photo_uri_key"; - private static final String HAS_PHOTO_KEY = "has_photo_key"; - - private static final int CAMERA_REQUEST_CODE = 5; - private static final int EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE = 6; // The following are set in onCreate - private boolean hasCamera; - private Uri photoUri; - private boolean hasPhoto; private ContentProviderUtils contentProviderUtils; private SharedPreferences sharedPreferences; private TrackRecordingServiceConnection trackRecordingServiceConnection; @@ -102,10 +87,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA @Override public void run() { trackController.update(isRecording(), recordingTrackPaused); - if (hasPhoto && photoUri != null) { - hasPhoto = false; - long markerId = trackRecordingServiceConnection.addMarker(TrackDetailActivity.this, null, null, null, photoUri.toString()); - } } }); } @@ -148,7 +129,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA }; private MenuItem insertMarkerMenuItem; - private MenuItem insertPhotoMenuItem; private MenuItem markerListMenuItem; private MenuItem shareMenuItem; @@ -182,10 +162,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA super.onCreate(savedInstanceState); recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this); - hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA); - photoUri = savedInstanceState != null ? (Uri) savedInstanceState.getParcelable(PHOTO_URI_KEY) : null; - hasPhoto = savedInstanceState != null && savedInstanceState.getBoolean(HAS_PHOTO_KEY, false); - contentProviderUtils = new ContentProviderUtils(this); handleIntent(getIntent()); @@ -307,23 +283,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(CURRENT_TAB_TAG_KEY, pager.getCurrentItem()); - - if (photoUri != null) { - outState.putParcelable(PHOTO_URI_KEY, photoUri); - } - outState.putBoolean(HAS_PHOTO_KEY, hasPhoto); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == CAMERA_REQUEST_CODE) { - if (resultCode == RESULT_CANCELED) { - Toast.makeText(this, R.string.marker_add_canceled, Toast.LENGTH_LONG).show(); - return; - } - hasPhoto = resultCode == RESULT_OK; - } - super.onActivityResult(requestCode, resultCode, data); } @Override @@ -343,8 +302,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA getMenuInflater().inflate(R.menu.track_detail, menu); insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker); - insertPhotoMenuItem = menu.findItem(R.id.track_detail_insert_photo); - insertPhotoMenuItem.setVisible(new Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(getPackageManager()) != null); shareMenuItem = menu.findItem(R.id.track_detail_share); markerListMenuItem = menu.findItem(R.id.track_detail_markers); @@ -372,9 +329,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA intent = Intent.createChooser(intent, null); startActivity(intent); return true; - case R.id.track_detail_insert_photo: - createWaypointWithPicture(); - return true; case R.id.track_detail_menu_show_on_map: IntentUtils.showTrackOnMap(this, new long[]{trackId}); return true; @@ -400,20 +354,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA } } - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == EXTERNAL_STORAGE_PERMISSION_REQUEST_CODE) { - if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { - Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show(); - } else { - createWaypointWithPicture(); - } - return; - } - - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - @Override protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() { return trackRecordingServiceConnection; @@ -471,7 +411,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA */ private void updateMenuItems(boolean isPaused) { insertMarkerMenuItem.setVisible(isRecording() && !isPaused); - insertPhotoMenuItem.setVisible(hasCamera && isRecording() && !isPaused); shareMenuItem.setVisible(!isRecording()); markerListMenuItem.setShowAsAction(isRecording() ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_IF_ROOM); String title; @@ -484,12 +423,6 @@ public class TrackDetailActivity extends AbstractListActivity implements ChooseA setTitle(title); } - private void createWaypointWithPicture() { - Pair intentAndPhotoUri = IntentUtils.createTakePictureIntent(this, trackId); - photoUri = intentAndPhotoUri.second; - startActivityForResult(intentAndPhotoUri.first, CAMERA_REQUEST_CODE); - } - public void chooseActivityType(String category) { ChooseActivityTypeDialogFragment.showDialog(getSupportFragmentManager(), category); } diff --git a/src/main/java/de/dennisguse/opentracks/util/FileUtils.java b/src/main/java/de/dennisguse/opentracks/util/FileUtils.java index 2a523a10d..3a3679c15 100644 --- a/src/main/java/de/dennisguse/opentracks/util/FileUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/FileUtils.java @@ -16,11 +16,21 @@ package de.dennisguse.opentracks.util; import android.content.Context; +import android.net.Uri; import android.os.Environment; +import android.util.Log; +import androidx.core.content.FileProvider; import androidx.documentfile.provider.DocumentFile; import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.text.SimpleDateFormat; +import java.util.Date; import de.dennisguse.opentracks.BuildConfig; @@ -31,6 +41,8 @@ import de.dennisguse.opentracks.BuildConfig; */ public class FileUtils { + private static final String TAG = FileUtils.class.getSimpleName(); + /** * Used to transfer picture from the camera. */ @@ -38,6 +50,8 @@ public class FileUtils { public static final String EXPORT_DIR = "OpenTracks"; + private static final String JPEG_EXTENSION = "jpeg"; + /** * The maximum FAT32 path length. See the FAT32 spec at * http://msdn.microsoft.com/en-us/windows/hardware/gg463080 @@ -196,4 +210,46 @@ public class FileUtils { return name; } + + /** + * Returns the image's absolute path from a track identified by trackId. + * + * @param context the context. + * @param trackId the track id. + */ + public static String getImageUrl(Context context, long trackId) { + File dir = FileUtils.getPhotoDir(context, trackId); + + String fileName = SimpleDateFormat.getDateTimeInstance().format(new Date()); + File file = new File(dir, FileUtils.buildUniqueFileName(dir, fileName, JPEG_EXTENSION)); + + return file.getAbsolutePath(); + } + + /** + * Copy a File (src) to a File (dst). + * + * @param src source file. + * @param dst destination file. + * @throws IOException + */ + public static void copy(FileDescriptor src, File dst) throws IOException { + try (FileChannel in = new FileInputStream(src).getChannel(); + FileChannel out = new FileOutputStream(dst).getChannel()) { + in.transferTo(0, in.size(), out); + } catch (Exception e) { + // post to log + Log.e(TAG, e.getMessage()); + } + } + + /** + * Returns a Uri for the file. + * + * @param context the context. + * @param file the file. + */ + public static Uri getUriForFile(Context context, File file) { + return FileProvider.getUriForFile(context, FileUtils.FILEPROVIDER, file); + } } diff --git a/src/main/res/drawable/ic_add_a_photo_24dp.xml b/src/main/res/drawable/ic_add_a_photo_24dp.xml new file mode 100644 index 000000000..e519d26c9 --- /dev/null +++ b/src/main/res/drawable/ic_add_a_photo_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/main/res/drawable/ic_add_photo_alternate_24dp.xml b/src/main/res/drawable/ic_add_photo_alternate_24dp.xml new file mode 100644 index 000000000..a63ecb49c --- /dev/null +++ b/src/main/res/drawable/ic_add_photo_alternate_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/main/res/drawable/shape_circle_darkgray.xml b/src/main/res/drawable/shape_circle_darkgray.xml new file mode 100644 index 000000000..f864113b4 --- /dev/null +++ b/src/main/res/drawable/shape_circle_darkgray.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/main/res/layout/marker_edit.xml b/src/main/res/layout/marker_edit.xml index 244e399c0..c7b0ea188 100644 --- a/src/main/res/layout/marker_edit.xml +++ b/src/main/res/layout/marker_edit.xml @@ -67,6 +67,39 @@ limitations under the License. android:imeOptions="actionDone" android:importantForAutofill="no" /> + + + + + + + diff --git a/src/main/res/menu/marker_edit.xml b/src/main/res/menu/marker_edit.xml new file mode 100644 index 000000000..1dccfbd1e --- /dev/null +++ b/src/main/res/menu/marker_edit.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/menu/track_detail.xml b/src/main/res/menu/track_detail.xml index 5282be8b5..b3ae34f7e 100644 --- a/src/main/res/menu/track_detail.xml +++ b/src/main/res/menu/track_detail.xml @@ -21,12 +21,6 @@ limitations under the License. android:orderInCategory="1" app:showAsAction="ifRoom" android:title="@string/menu_insert_marker" /> - Importar todo Insertar marcador Insertar foto + Insertar imagen de galería Mapa Marcadores Reproducir diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 339f9c20a..2547adc0d 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -169,6 +169,7 @@ No se puede insertar el marcador. No se recibe señal de GPS. Inténtalo de nuevo. Se ha insertado un marcador Cancelado, no se ha añadido ningún marcador + Cancelado, no se ha añadido la foto al marcador Todos los marcadores de esta ruta se eliminarán del dispositivo de forma permanente El marcador se eliminará del dispositivo de forma permanente ¿Eliminar marcador? @@ -195,6 +196,7 @@ Importar todo Insertar marcador Insertar foto + Insertar imagen de galería Mapa Marcadores Reproducir diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index f5f52b20a..55bfe0c1c 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -275,6 +275,7 @@ limitations under the License. Unable to insert a marker. No GPS signal. Try again. A marker was inserted Canceled, no marker added + Canceled, no photo marker added All markers for this track will be permanently deleted from the device The marker will be permanently deleted from the device Delete marker? @@ -305,6 +306,7 @@ limitations under the License. Import all Insert marker Insert photo + Insert gallery image Map Markers Play