diff --git a/OsmAnd/res/layout/bottom_sheet_with_progress_bar.xml b/OsmAnd/res/layout/bottom_sheet_with_progress_bar.xml new file mode 100644 index 0000000000..258489706b --- /dev/null +++ b/OsmAnd/res/layout/bottom_sheet_with_progress_bar.xml @@ -0,0 +1,48 @@ + + + + + + + + + + \ No newline at end of file diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index 439678fcac..7cd16f7f7e 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -13,6 +13,10 @@ --> Select edits for upload + Uploaded %1$d of %2$d + Uploading %1$d of %2$d + Upload completed + Uploading Copy to favorites Copy to map markers Delete waypoints diff --git a/OsmAnd/src/net/osmand/plus/dialogs/UploadPhotoProgressBottomSheet.java b/OsmAnd/src/net/osmand/plus/dialogs/UploadPhotoProgressBottomSheet.java new file mode 100644 index 0000000000..f1c37a55ee --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/dialogs/UploadPhotoProgressBottomSheet.java @@ -0,0 +1,118 @@ +package net.osmand.plus.dialogs; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; + +import net.osmand.plus.R; +import net.osmand.plus.UiUtilities; +import net.osmand.plus.base.MenuBottomSheetDialogFragment; +import net.osmand.plus.base.bottomsheetmenu.BaseBottomSheetItem; +import net.osmand.plus.base.bottomsheetmenu.BottomSheetItemWithDescription; +import net.osmand.plus.base.bottomsheetmenu.simpleitems.DividerSpaceItem; +import net.osmand.plus.mapcontextmenu.UploadPhotosAsyncTask.UploadPhotosListener; + +public class UploadPhotoProgressBottomSheet extends MenuBottomSheetDialogFragment implements UploadPhotosListener { + + public static final String TAG = UploadPhotoProgressBottomSheet.class.getSimpleName(); + + private ProgressBar progressBar; + private TextView uploadedPhotosTitle; + private TextView uploadedPhotosCounter; + + private OnDismissListener onDismissListener; + + private int progress; + private int maxProgress; + + @Override + public void createMenuItems(Bundle savedInstanceState) { + Context context = requireContext(); + LayoutInflater inflater = UiUtilities.getInflater(context, nightMode); + View view = inflater.inflate(R.layout.bottom_sheet_with_progress_bar, null); + + uploadedPhotosTitle = view.findViewById(R.id.title); + uploadedPhotosCounter = view.findViewById(R.id.description); + progressBar = view.findViewById(R.id.progress_bar); + progressBar.setMax(maxProgress); + String titleProgress = getString(progress == maxProgress? R.string.upload_photo_completed: R.string.upload_photo); + String descriptionProgress; + if (progress == maxProgress) { + descriptionProgress = getString(R.string.uploaded_count, progress, maxProgress); + } else { + descriptionProgress = getString(R.string.uploading_count, progress, maxProgress); + } + + BaseBottomSheetItem descriptionItem = new BottomSheetItemWithDescription.Builder() + .setDescription(descriptionProgress) + .setTitle(titleProgress) + .setCustomView(view) + .create(); + items.add(descriptionItem); + + updateProgress(progress); + + int padding = getResources().getDimensionPixelSize(R.dimen.content_padding_small); + items.add(new DividerSpaceItem(context, padding)); + } + + public void setMaxProgress(int maxProgress) { + this.maxProgress = maxProgress; + } + + public void setOnDismissListener(OnDismissListener onDismissListener) { + this.onDismissListener = onDismissListener; + } + + private void updateProgress(int progress) { + progressBar.setProgress(progress); + uploadedPhotosCounter.setText((getString(R.string.uploading_count, progress, maxProgress))); + uploadedPhotosTitle.setText(progress == maxProgress ? R.string.upload_photo_completed : R.string.upload_photo); + } + + @Override + public void uploadPhotosProgressUpdate(int progress) { + this.progress = progress; + updateProgress(progress); + } + + @Override + public void uploadPhotosFinished() { + updateProgress(maxProgress); + if (progress == maxProgress) { + uploadedPhotosCounter.setText((getString(R.string.uploaded_count, progress, maxProgress))); + setDismissButtonTextId(R.string.shared_string_close); + UiUtilities.setupDialogButton(nightMode, dismissButton, getDismissButtonType(), getDismissButtonTextId()); + } + } + + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + FragmentActivity activity = getActivity(); + if (onDismissListener != null && activity != null && !activity.isChangingConfigurations()) { + onDismissListener.onDismiss(dialog); + } + } + + public static UploadPhotosListener showInstance(@NonNull FragmentManager fragmentManager, int maxProgress, OnDismissListener listener) { + UploadPhotoProgressBottomSheet fragment = new UploadPhotoProgressBottomSheet(); + fragment.setRetainInstance(true); + fragment.setMaxProgress(maxProgress); + fragment.setOnDismissListener(listener); + fragmentManager.beginTransaction() + .add(fragment, UploadPhotoProgressBottomSheet.TAG) + .commitAllowingStateLoss(); + + return fragment; + } +} \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuBuilder.java b/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuBuilder.java index 3890046fac..183e41a84f 100644 --- a/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuBuilder.java +++ b/OsmAnd/src/net/osmand/plus/mapcontextmenu/MenuBuilder.java @@ -1,22 +1,19 @@ package net.osmand.plus.mapcontextmenu; import android.app.Activity; +import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.res.ColorStateList; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.graphics.Color; -import android.graphics.Matrix; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.net.Uri; import android.os.AsyncTask; -import android.os.Handler; -import android.os.Looper; +import android.os.Build; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; @@ -39,14 +36,12 @@ import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import net.osmand.AndroidUtils; -import net.osmand.PlatformUtil; import net.osmand.data.Amenity; import net.osmand.data.LatLon; import net.osmand.data.PointDescription; import net.osmand.data.QuadRect; import net.osmand.osm.PoiCategory; import net.osmand.osm.PoiType; -import net.osmand.osm.io.NetworkUtils; import net.osmand.plus.OsmAndFormatter; import net.osmand.plus.OsmandApplication; import net.osmand.plus.OsmandPlugin; @@ -54,6 +49,7 @@ import net.osmand.plus.R; import net.osmand.plus.UiUtilities; import net.osmand.plus.Version; import net.osmand.plus.activities.ActivityResultListener; +import net.osmand.plus.activities.ActivityResultListener.OnActivityResultListener; import net.osmand.plus.activities.MapActivity; import net.osmand.plus.helpers.FontCache; import net.osmand.plus.mapcontextmenu.builders.cards.AbstractCard; @@ -79,13 +75,6 @@ import net.osmand.plus.widgets.tools.ClickableSpanTouchListener; import net.osmand.util.Algorithms; import net.osmand.util.MapUtils; -import org.apache.commons.logging.Log; -import org.openplacereviews.opendb.util.exception.FailedVerificationException; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -99,8 +88,6 @@ import static net.osmand.plus.mapcontextmenu.builders.cards.ImageCard.GetImageCa public class MenuBuilder { private static final int PICK_IMAGE = 1231; - private static final int MAX_IMAGE_LENGTH = 2048; - private static final Log LOG = PlatformUtil.getLog(MenuBuilder.class); public static final float SHADOW_HEIGHT_TOP_DP = 17f; public static final int TITLE_LIMIT = 60; protected static final String[] arrowChars = new String[] {"=>", " - "}; @@ -133,7 +120,6 @@ public class MenuBuilder { private String preferredMapLang; private String preferredMapAppLang; private boolean transliterateNames; - private View view; private View photoButton; private final OpenDBAPI openDBAPI = new OpenDBAPI(); @@ -270,7 +256,6 @@ public class MenuBuilder { } public void build(View view) { - this.view = view; firstRow = true; hidden = false; buildTopInternal(view); @@ -425,7 +410,7 @@ public class MenuBuilder { if (false) { AddPhotosBottomSheetDialogFragment.showInstance(mapActivity.getSupportFragmentManager()); } else { - registerResultListener(view); + registerResultListener(); final String baseUrl = OPRConstants.getBaseUrl(app); final String name = app.getSettings().OPR_USERNAME.get(); final String privateKey = app.getSettings().OPR_ACCESS_TOKEN.get(); @@ -443,6 +428,9 @@ public class MenuBuilder { Intent intent = new Intent(); intent.setType("image/*"); intent.setAction(Intent.ACTION_GET_CONTENT); + if (Build.VERSION.SDK_INT > 18) { + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + } mapActivity.startActivityForResult(Intent.createChooser(intent, mapActivity.getString(R.string.select_picture)), PICK_IMAGE); } @@ -472,132 +460,33 @@ public class MenuBuilder { false, null, false); } - private void registerResultListener(final View view) { - mapActivity.registerActivityResultListener(new ActivityResultListener(PICK_IMAGE, new ActivityResultListener. - OnActivityResultListener() { + private void registerResultListener() { + mapActivity.registerActivityResultListener(new ActivityResultListener(PICK_IMAGE, new OnActivityResultListener() { @Override public void onResult(int resultCode, Intent resultData) { if (resultData != null) { - handleSelectedImage(view, resultData.getData()); + List imagesUri = new ArrayList<>(); + Uri data = resultData.getData(); + if (data != null) { + imagesUri.add(data); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + ClipData clipData = resultData.getClipData(); + if (clipData != null) { + for (int i = 0; i < clipData.getItemCount(); i++) { + Uri uri = resultData.getClipData().getItemAt(i).getUri(); + if (uri != null) { + imagesUri.add(uri); + } + } + } + } + execute(new UploadPhotosAsyncTask(mapActivity, imagesUri, getLatLon(), placeId, getAdditionalCardParams(), imageCardListener)); } } })); } - private void handleSelectedImage(final View view, final Uri uri) { - Thread t = new Thread(new Runnable() { - @Override - public void run() { - InputStream inputStream = null; - try { - inputStream = app.getContentResolver().openInputStream(uri); - if (inputStream != null) { - uploadImageToPlace(inputStream); - } - } catch (Exception e) { - LOG.error(e); - String str = app.getString(R.string.cannot_upload_image); - showToastMessage(str); - } finally { - Algorithms.closeStream(inputStream); - } - } - }); - t.start(); - } - - private void uploadImageToPlace(InputStream image) { - InputStream serverData = new ByteArrayInputStream(compressImageToJpeg(image)); - final String baseUrl = OPRConstants.getBaseUrl(app); - // all these should be constant - String url = baseUrl + "api/ipfs/image"; - String response = NetworkUtils.sendPostDataRequest(url, "file", "compressed.jpeg", serverData); - if (response != null) { - int res = 0; - try { - StringBuilder error = new StringBuilder(); - String privateKey = app.getSettings().OPR_ACCESS_TOKEN.get(); - String username = app.getSettings().OPR_USERNAME.get(); - res = openDBAPI.uploadImage( - placeId, - baseUrl, - privateKey, - username, - response, error); - if (res != 200) { - showToastMessage(error.toString()); - } else { - //ok, continue - } - } catch (FailedVerificationException e) { - LOG.error(e); - checkTokenAndShowScreen(); - } - if (res != 200) { - //image was uploaded but not added to blockchain - checkTokenAndShowScreen(); - } else { - String str = app.getString(R.string.successfully_uploaded_pattern, 1, 1); - showToastMessage(str); - //refresh the image - execute(new GetImageCardsTask(mapActivity, getLatLon(), getAdditionalCardParams(), imageCardListener)); - } - } else { - checkTokenAndShowScreen(); - } - } - - private void showToastMessage(final String str) { - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - Toast.makeText(mapActivity.getBaseContext(), str, Toast.LENGTH_LONG).show(); - } - }); - } - - //This method runs on non main thread - private void checkTokenAndShowScreen() { - final String baseUrl = OPRConstants.getBaseUrl(app); - final String name = app.getSettings().OPR_USERNAME.get(); - final String privateKey = app.getSettings().OPR_ACCESS_TOKEN.get(); - if (openDBAPI.checkPrivateKeyValid(baseUrl, name, privateKey)) { - String str = app.getString(R.string.cannot_upload_image); - showToastMessage(str); - } else { - app.runInUIThread(new Runnable() { - @Override - public void run() { - OprStartFragment.showInstance(mapActivity.getSupportFragmentManager()); - } - }); - } - } - - private byte[] compressImageToJpeg(InputStream image) { - BufferedInputStream bufferedInputStream = new BufferedInputStream(image); - Bitmap bmp = BitmapFactory.decodeStream(bufferedInputStream); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - int h = bmp.getHeight(); - int w = bmp.getWidth(); - boolean scale = false; - while (w > MAX_IMAGE_LENGTH || h > MAX_IMAGE_LENGTH) { - w = w / 2; - h = h / 2; - scale = true; - } - if (scale) { - Matrix matrix = new Matrix(); - matrix.postScale(w, h); - Bitmap resizedBitmap = Bitmap.createBitmap( - bmp, 0, 0, w, h, matrix, false); - bmp.recycle(); - bmp = resizedBitmap; - } - bmp.compress(Bitmap.CompressFormat.JPEG, 90, os); - return os.toByteArray(); - } - private void startLoadingImages() { if (onlinePhotoCardsRow == null) { return; diff --git a/OsmAnd/src/net/osmand/plus/mapcontextmenu/UploadPhotosAsyncTask.java b/OsmAnd/src/net/osmand/plus/mapcontextmenu/UploadPhotosAsyncTask.java new file mode 100644 index 0000000000..30932e8b42 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/mapcontextmenu/UploadPhotosAsyncTask.java @@ -0,0 +1,220 @@ +package net.osmand.plus.mapcontextmenu; + +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.net.Uri; +import android.os.AsyncTask; + +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; + +import net.osmand.AndroidUtils; +import net.osmand.PlatformUtil; +import net.osmand.data.LatLon; +import net.osmand.osm.io.NetworkUtils; +import net.osmand.plus.OsmandApplication; +import net.osmand.plus.R; +import net.osmand.plus.activities.MapActivity; +import net.osmand.plus.dialogs.UploadPhotoProgressBottomSheet; +import net.osmand.plus.mapcontextmenu.builders.cards.ImageCard.GetImageCardsTask; +import net.osmand.plus.mapcontextmenu.builders.cards.ImageCard.GetImageCardsTask.GetImageCardsListener; +import net.osmand.plus.openplacereviews.OPRConstants; +import net.osmand.plus.openplacereviews.OprStartFragment; +import net.osmand.plus.osmedit.opr.OpenDBAPI; +import net.osmand.util.Algorithms; + +import org.apache.commons.logging.Log; +import org.openplacereviews.opendb.util.exception.FailedVerificationException; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.Map; + +public class UploadPhotosAsyncTask extends AsyncTask { + + private static final Log LOG = PlatformUtil.getLog(UploadPhotosAsyncTask.class); + + private static final int MAX_IMAGE_LENGTH = 2048; + + private final OsmandApplication app; + private final WeakReference activityRef; + private UploadPhotosListener listener; + + private final OpenDBAPI openDBAPI = new OpenDBAPI(); + private final LatLon latLon; + private final List data; + private final String[] placeId; + private final Map params; + private final GetImageCardsListener imageCardListener; + + public UploadPhotosAsyncTask(MapActivity activity, List data, LatLon latLon, String[] placeId, + Map params, GetImageCardsListener imageCardListener) { + app = (OsmandApplication) activity.getApplicationContext(); + activityRef = new WeakReference<>(activity); + this.data = data; + this.latLon = latLon; + this.params = params; + this.placeId = placeId; + this.imageCardListener = imageCardListener; + } + + @Override + protected void onPreExecute() { + FragmentActivity activity = activityRef.get(); + if (AndroidUtils.isActivityNotDestroyed(activity)) { + FragmentManager manager = activity.getSupportFragmentManager(); + listener = UploadPhotoProgressBottomSheet.showInstance(manager, data.size(), new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + cancel(false); + } + }); + } + } + + @Override + protected void onProgressUpdate(Integer... values) { + if (listener != null) { + listener.uploadPhotosProgressUpdate(values[0]); + } + } + + protected Void doInBackground(Void... uris) { + for (int i = 0; i < data.size(); i++) { + if (isCancelled()) { + break; + } + Uri uri = data.get(i); + handleSelectedImage(uri); + publishProgress(i + 1); + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + if (listener != null) { + listener.uploadPhotosFinished(); + } + } + + private void handleSelectedImage(final Uri uri) { + InputStream inputStream = null; + try { + inputStream = app.getContentResolver().openInputStream(uri); + if (inputStream != null) { + uploadImageToPlace(inputStream); + } + } catch (Exception e) { + LOG.error(e); + app.showToastMessage(R.string.cannot_upload_image); + } finally { + Algorithms.closeStream(inputStream); + } + } + + private void uploadImageToPlace(InputStream image) { + InputStream serverData = new ByteArrayInputStream(compressImageToJpeg(image)); + final String baseUrl = OPRConstants.getBaseUrl(app); + // all these should be constant + String url = baseUrl + "api/ipfs/image"; + String response = NetworkUtils.sendPostDataRequest(url, "file", "compressed.jpeg", serverData); + if (response != null) { + int res = 0; + try { + StringBuilder error = new StringBuilder(); + String privateKey = app.getSettings().OPR_ACCESS_TOKEN.get(); + String username = app.getSettings().OPR_USERNAME.get(); + res = openDBAPI.uploadImage( + placeId, + baseUrl, + privateKey, + username, + response, error); + if (res != 200) { + app.showToastMessage(error.toString()); + } else { + //ok, continue + } + } catch (FailedVerificationException e) { + LOG.error(e); + checkTokenAndShowScreen(); + } + if (res != 200) { + //image was uploaded but not added to blockchain + checkTokenAndShowScreen(); + } else { + String str = app.getString(R.string.successfully_uploaded_pattern, 1, 1); + app.showToastMessage(str); + //refresh the image + + MapActivity activity = activityRef.get(); + if (activity != null) { + MenuBuilder.execute(new GetImageCardsTask(activity, latLon, params, imageCardListener)); + } + } + } else { + checkTokenAndShowScreen(); + } + } + + //This method runs on non main thread + private void checkTokenAndShowScreen() { + String baseUrl = OPRConstants.getBaseUrl(app); + String name = app.getSettings().OPR_USERNAME.get(); + String privateKey = app.getSettings().OPR_ACCESS_TOKEN.get(); + if (openDBAPI.checkPrivateKeyValid(baseUrl, name, privateKey)) { + app.showToastMessage(R.string.cannot_upload_image); + } else { + app.runInUIThread(new Runnable() { + @Override + public void run() { + MapActivity activity = activityRef.get(); + if (activity != null) { + OprStartFragment.showInstance(activity.getSupportFragmentManager()); + } + } + }); + } + } + + private byte[] compressImageToJpeg(InputStream image) { + BufferedInputStream bufferedInputStream = new BufferedInputStream(image); + Bitmap bmp = BitmapFactory.decodeStream(bufferedInputStream); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + int h = bmp.getHeight(); + int w = bmp.getWidth(); + boolean scale = false; + while (w > MAX_IMAGE_LENGTH || h > MAX_IMAGE_LENGTH) { + w = w / 2; + h = h / 2; + scale = true; + } + if (scale) { + Matrix matrix = new Matrix(); + matrix.postScale(w, h); + Bitmap resizedBitmap = Bitmap.createBitmap( + bmp, 0, 0, w, h, matrix, false); + bmp.recycle(); + bmp = resizedBitmap; + } + bmp.compress(Bitmap.CompressFormat.JPEG, 90, os); + return os.toByteArray(); + } + + + public interface UploadPhotosListener { + + void uploadPhotosProgressUpdate(int progress); + + void uploadPhotosFinished(); + + } +} \ No newline at end of file