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..fd1f27bbae --- /dev/null +++ b/OsmAnd/res/layout/bottom_sheet_with_progress_bar.xml @@ -0,0 +1,49 @@ + + + + + + + + + + \ No newline at end of file diff --git a/OsmAnd/res/values/strings.xml b/OsmAnd/res/values/strings.xml index 79d3a211b4..4b4b94826e 100644 --- a/OsmAnd/res/values/strings.xml +++ b/OsmAnd/res/values/strings.xml @@ -11,7 +11,10 @@ Thx - Hardy --> - + Uploaded %1$s of %2$d + Uploading %1$s of %2$d + Upload completed + Uploading Copy to favorites Copy to map markers Delete waypoints diff --git a/OsmAnd/src/net/osmand/plus/dialogs/UploadPhotoWithProgressBarBottomSheet.java b/OsmAnd/src/net/osmand/plus/dialogs/UploadPhotoWithProgressBarBottomSheet.java new file mode 100644 index 0000000000..73e09845a3 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/dialogs/UploadPhotoWithProgressBarBottomSheet.java @@ -0,0 +1,111 @@ +package net.osmand.plus.dialogs; + +import android.app.Activity; +import android.content.Context; +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.annotation.Nullable; +import androidx.fragment.app.FragmentManager; + +import net.osmand.plus.R; +import net.osmand.plus.UiUtilities; +import net.osmand.plus.UiUtilities.DialogButtonType; +import net.osmand.plus.activities.MapActivity; +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.widgets.TextViewEx; + +public class UploadPhotoWithProgressBarBottomSheet extends MenuBottomSheetDialogFragment { + + public static final String TAG = UploadPhotoWithProgressBarBottomSheet.class.getSimpleName(); + + private ProgressBar progressBar; + private TextView uploadedPhotosCounter; + private TextViewEx uploadedPhotosTitle; + 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); + + progressBar = view.findViewById(R.id.progress_bar); + progressBar.setMax(maxProgress); + uploadedPhotosCounter = view.findViewById(R.id.description); + + uploadedPhotosTitle = view.findViewById(R.id.title); + + BaseBottomSheetItem descriptionItem = new BottomSheetItemWithDescription.Builder() + .setDescription(getString(R.string.uploading_count, progressBar.getProgress(), maxProgress)) + .setTitle(getString(R.string.upload_photo)) + .setCustomView(view) + .create(); + items.add(descriptionItem); + + int padding = getResources().getDimensionPixelSize(R.dimen.content_padding_small); + items.add(new DividerSpaceItem(requireContext(), padding)); + } + + public void onProgressUpdate(int progress) { + progressBar.setProgress(progress); + uploadedPhotosCounter.setText((getString(R.string.uploading_count, progressBar.getProgress(), maxProgress))); + } + + public void onUploadingFinished() { + TextViewEx dismissButtonText = dismissButton.findViewById(R.id.button_text); + setDismissButtonTextId(R.string.shared_string_close); + dismissButtonText.setText(R.string.shared_string_close); + uploadedPhotosTitle.setText(R.string.upload_photo_completed); + uploadedPhotosCounter.setText((getString(R.string.uploaded_count, progressBar.getProgress(), maxProgress))); + } + + @Override + protected boolean useVerticalButtons() { + return true; + } + + @Override + protected int getDismissButtonTextId() { + return R.string.shared_string_cancel; + } + + @Override + protected DialogButtonType getRightBottomButtonType() { + return DialogButtonType.PRIMARY; + } + + @Override + public int getSecondDividerHeight() { + return getResources().getDimensionPixelSize(R.dimen.bottom_sheet_icon_margin); + } + + @Override + protected void onRightBottomButtonClick() { + dismiss(); + } + + @Nullable + public MapActivity getMapActivity() { + Activity activity = getActivity(); + if (activity instanceof MapActivity) { + return (MapActivity) activity; + } + return null; + } + + public static UploadPhotoWithProgressBarBottomSheet showInstance(@NonNull FragmentManager fragmentManager, int maxProgress) { + UploadPhotoWithProgressBarBottomSheet fragment = new UploadPhotoWithProgressBarBottomSheet(); + fragment.maxProgress = maxProgress; + fragment.setRetainInstance(true); + fragmentManager.beginTransaction().add(fragment, 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..98198d65f3 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; @@ -46,7 +43,6 @@ 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 +50,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; @@ -80,12 +77,7 @@ 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,7 +91,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; @@ -425,7 +416,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 +434,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 +466,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..ce59999a11 --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/mapcontextmenu/UploadPhotosAsyncTask.java @@ -0,0 +1,203 @@ +package net.osmand.plus.mapcontextmenu; + +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 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.UploadPhotoWithProgressBarBottomSheet; +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 UploadPhotoWithProgressBarBottomSheet progress; + + 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)) { + progress = UploadPhotoWithProgressBarBottomSheet.showInstance(activity.getSupportFragmentManager(), data.size()); + } + } + + 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); + } + return null; + } + + @Override + protected void onProgressUpdate(Integer... values) { + if (progress != null) { + progress.onProgressUpdate(values[0]); + } + } + + @Override + protected void onPostExecute(Void aVoid) { + if (progress != null) { + progress.onUploadingFinished(); + } + } + + 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() { + 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); + app.showToastMessage(str); + } 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(); + } +} \ No newline at end of file