package app.fedilab.android.mastodon.helper; /* Copyright 2022 Thomas Schneider * * This file is a part of Fedilab * * This program 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. * * Fedilab 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 Fedilab; if not, * see . */ import static android.content.Context.DOWNLOAD_SERVICE; import static app.fedilab.android.mastodon.helper.LogoHelper.getMainLogo; import android.app.Activity; import android.app.DownloadManager; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.graphics.drawable.Drawable; import android.media.MediaRecorder; import android.media.MediaScannerConnection; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.MediaStore; import android.text.format.DateFormat; import android.view.View; import android.webkit.MimeTypeMap; import android.webkit.URLUtil; import android.widget.RelativeLayout; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.content.FileProvider; import androidx.exifinterface.media.ExifInterface; import com.bumptech.glide.Glide; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import com.github.piasy.rxandroidaudio.AudioRecorder; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.channels.FileChannel; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.atomic.AtomicInteger; import app.fedilab.android.BuildConfig; import app.fedilab.android.R; import app.fedilab.android.activities.MainActivity; import app.fedilab.android.databinding.DatetimePickerBinding; import app.fedilab.android.databinding.PopupRecordBinding; import app.fedilab.android.mastodon.activities.ComposeActivity; import app.fedilab.android.mastodon.client.entities.api.Attachment; import es.dmoral.toasty.Toasty; public class MediaHelper { /** * Manage downloads with URLs, does not concern images, they are moved with Glide cache. * * @param context Context * @param url String download url */ public static long manageDownloadsNoPopup(final Context context, final String url) { final DownloadManager.Request request; try { request = new DownloadManager.Request(Uri.parse(url.trim())); } catch (Exception e) { e.printStackTrace(); Toasty.error(context, context.getString(R.string.toast_error), Toast.LENGTH_LONG).show(); return -1; } try { String mime = getMimeType(url); String fileName = URLUtil.guessFileName(url, null, null); if (fileName.endsWith(".bin")) { fileName = fileName.replace(".bin", ".mp4"); } request.allowScanningByMediaScanner(); if (mime.toLowerCase().startsWith("video")) { request.setDestinationInExternalPublicDir(Environment.DIRECTORY_MOVIES, context.getString(R.string.app_name) + "/" + fileName); } else if (mime.toLowerCase().startsWith("audio")) { request.setDestinationInExternalPublicDir(Environment.DIRECTORY_MUSIC, context.getString(R.string.app_name) + "/" + fileName); } else { request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); } request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); DownloadManager dm = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE); return dm.enqueue(request); } catch (Exception e) { Toasty.error(context, context.getString(R.string.error_destination_path), Toast.LENGTH_LONG).show(); e.printStackTrace(); return -1; } } /** * Download from Glid cache * * @param context Context * @param url String */ public static void manageMove(Context context, String url, boolean share) { Glide.with(context) .asFile() .load(url) .into(new CustomTarget() { @Override public void onResourceReady(@NotNull File file, Transition transition) { String fileName = URLUtil.guessFileName(url, null, null); if (fileName.endsWith(".bin")) { fileName = fileName.replace(".bin", ".jpg"); } File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); File targeted_folder = new File(path, context.getString(R.string.app_name)); if (!targeted_folder.exists()) { boolean created = targeted_folder.mkdir(); if (!created) { Toasty.error(context, context.getString(R.string.toast_error), Toasty.LENGTH_SHORT).show(); return; } } FileInputStream fis = null; FileOutputStream fos = null; FileChannel in = null; FileChannel out = null; try { File backupFile = new File(targeted_folder.getAbsolutePath() + "/" + fileName); //noinspection ResultOfMethodCallIgnored backupFile.createNewFile(); fis = new FileInputStream(file); fos = new FileOutputStream(backupFile); in = fis.getChannel(); out = fos.getChannel(); long size = in.size(); in.transferTo(0, size, out); String mime = getMimeType(url); final Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); Uri uri = Uri.fromFile(backupFile); intent.setDataAndType(uri, mime); MediaScannerConnection.scanFile(context, new String[]{backupFile.getAbsolutePath()}, null, null); if (!share) { Helper.notify_user(context, Helper.getCurrentAccount(context), intent, BitmapFactory.decodeResource(context.getResources(), getMainLogo(context)), Helper.NotifType.STORE, context.getString(R.string.save_over), context.getString(R.string.download_from, fileName)); Toasty.success(context, context.getString(R.string.save_over), Toasty.LENGTH_LONG).show(); } else { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_STREAM, uri); shareIntent.setType(mime); try { context.startActivity(shareIntent); } catch (Exception ignored) { } } } catch (Throwable e) { e.printStackTrace(); } finally { try { if (fis != null) fis.close(); } catch (Throwable ignore) { } try { if (fos != null) fos.close(); } catch (Throwable ignore) { } try { if (in != null && in.isOpen()) in.close(); } catch (Throwable ignore) { } try { if (out != null && out.isOpen()) out.close(); } catch (Throwable ignore) { } } } @Override public void onLoadCleared(@Nullable Drawable placeholder) { } }); } public static String getMimeType(String url) { String type = null; String extension = MimeTypeMap.getFileExtensionFromUrl(url); if (extension != null) { type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); } return type; } public static Uri dispatchTakePictureIntent(Activity activity) { Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); Uri photoFileUri = null; // Ensure that there's a camera activity to handle the intent if (takePictureIntent.resolveActivity(activity.getPackageManager()) != null) { // Create the File where the photo should go File photoFile = null; try { photoFile = createImageFile(activity); } catch (IOException ignored) { Toasty.error(activity, activity.getString(R.string.toot_select_image_error), Toast.LENGTH_LONG).show(); } // Continue only if the File was successfully created if (photoFile != null) { photoFileUri = FileProvider.getUriForFile(activity, BuildConfig.APPLICATION_ID + ".fileProvider", photoFile); } takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoFileUri); activity.startActivityForResult(takePictureIntent, ComposeActivity.TAKE_PHOTO); } return photoFileUri; } private static File createImageFile(Context context) throws IOException { // Create an image file name String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH).format(new Date()); String imageFileName = "JPEG_" + timeStamp + "_"; File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); // Save a file: path for use with ACTION_VIEW intents return File.createTempFile( imageFileName, /* prefix */ ".jpg", /* suffix */ storageDir /* directory */ ); } /** * Record media * * @param activity Activity * @param listener ActionRecord */ public static void recordAudio(Activity activity, ActionRecord listener) { //String filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + "/fedilab_recorded_audio.ogg"; String filePath = activity.getCacheDir() + "/fedilab_recorded_audio.ogg"; AudioRecorder mAudioRecorder = AudioRecorder.getInstance(); File mAudioFile = new File(filePath); PopupRecordBinding binding = PopupRecordBinding.inflate(activity.getLayoutInflater()); AlertDialog.Builder audioPopup = new MaterialAlertDialogBuilder(activity); audioPopup.setView(binding.getRoot()); AlertDialog alert = audioPopup.create(); alert.show(); Timer timer = new Timer(); AtomicInteger count = new AtomicInteger(); timer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { activity.runOnUiThread(() -> { int value = count.getAndIncrement(); String minutes = "00"; String seconds; if (value > 60) { minutes = String.valueOf(value / 60); seconds = String.valueOf(value % 60); } else { seconds = String.valueOf(value); } if (minutes.length() == 1) { minutes = "0" + minutes; } if (seconds.length() == 1) { seconds = "0" + seconds; } binding.counter.setText(String.format(Locale.getDefault(), "%s:%s", minutes, seconds)); }); } }, 0, 1000); binding.record.setOnClickListener(v -> { mAudioRecorder.stopRecord(); timer.cancel(); alert.dismiss(); listener.onRecorded(filePath); }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { mAudioRecorder.prepareRecord(MediaRecorder.AudioSource.MIC, MediaRecorder.OutputFormat.OGG, MediaRecorder.AudioEncoder.OPUS, 48000, 384000, mAudioFile); } mAudioRecorder.startRecord(); } /** * Schedule a message * * @param activity - Activity * @param listener - OnSchedule */ public static void scheduleMessage(Activity activity, OnSchedule listener) { AlertDialog.Builder dialogBuilder = new MaterialAlertDialogBuilder(activity); DatetimePickerBinding binding = DatetimePickerBinding.inflate(activity.getLayoutInflater()); dialogBuilder.setView(binding.getRoot()); final AlertDialog alertDialog = dialogBuilder.create(); if (DateFormat.is24HourFormat(activity)) { binding.timePicker.setIs24HourView(true); } //Buttons management binding.dateTimeCancel.setOnClickListener(v -> alertDialog.dismiss()); binding.dateTimeNext.setOnClickListener(v -> { binding.datePicker.setVisibility(View.GONE); binding.timePicker.setVisibility(View.VISIBLE); binding.dateTimePrevious.setVisibility(View.VISIBLE); binding.dateTimeNext.setVisibility(View.GONE); binding.dateTimeSet.setVisibility(View.VISIBLE); }); binding.dateTimePrevious.setOnClickListener(v -> { binding.datePicker.setVisibility(View.VISIBLE); binding.timePicker.setVisibility(View.GONE); binding.dateTimePrevious.setVisibility(View.GONE); binding.dateTimeNext.setVisibility(View.VISIBLE); binding.dateTimeSet.setVisibility(View.GONE); }); binding.dateTimeSet.setOnClickListener(v -> { int hour, minute; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { hour = binding.timePicker.getHour(); minute = binding.timePicker.getMinute(); } else { hour = binding.timePicker.getCurrentHour(); minute = binding.timePicker.getCurrentMinute(); } Calendar calendar = new GregorianCalendar(binding.datePicker.getYear(), binding.datePicker.getMonth(), binding.datePicker.getDayOfMonth(), hour, minute); final long[] time = {calendar.getTimeInMillis()}; if ((time[0] - new Date().getTime()) < 60000) { Toasty.warning(activity, activity.getString(R.string.toot_scheduled_date), Toast.LENGTH_LONG).show(); } else { SimpleDateFormat sdf = new SimpleDateFormat(Helper.SCHEDULE_DATE_FORMAT, Locale.getDefault()); String date = sdf.format(calendar.getTime()); listener.scheduledAt(date); alertDialog.dismiss(); } }); alertDialog.show(); } /** * Returns the max height of a list of media * * @param attachmentList - List * @return int - The max height */ @SuppressWarnings("unused") public static int returnMaxHeightForPreviews(Context context, List attachmentList) { int maxHeight = RelativeLayout.LayoutParams.WRAP_CONTENT; if (attachmentList != null && attachmentList.size() > 0) { for (Attachment attachment : attachmentList) { if (attachment.meta != null && attachment.meta.getSmall() != null && attachment.meta.getSmall().height > maxHeight) { maxHeight = (int) Helper.convertDpToPixel(attachment.meta.getSmall().height, context); } } } return maxHeight; } public static void ResizedImageRequestBody(Context context, Uri uri, File targetedFile) { InputStream decodeBitmapInputStream = null; try { InputStream inputStream = context.getContentResolver().openInputStream(uri); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeStream(inputStream, null, options); try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } int orientation = getImageOrientation(uri, context.getContentResolver()); int scaledImageSize = 1024; final int maxRetry = 3; int retry = 0; do { FileOutputStream outputStream = new FileOutputStream(targetedFile); decodeBitmapInputStream = context.getContentResolver().openInputStream(uri); options.inSampleSize = calculateInSampleSize(options, scaledImageSize, scaledImageSize); options.inJustDecodeBounds = false; Bitmap scaledBitmap = BitmapFactory.decodeStream(decodeBitmapInputStream, null, options); Bitmap reorientedBitmap = reorientBitmap(scaledBitmap, orientation); if (reorientedBitmap == null) { scaledBitmap.recycle(); return; } Bitmap.CompressFormat format; if (!reorientedBitmap.hasAlpha()) { format = Bitmap.CompressFormat.JPEG; } else { format = Bitmap.CompressFormat.PNG; } reorientedBitmap.compress(format, 100, outputStream); reorientedBitmap.recycle(); scaledImageSize /= 2; retry++; } while (targetedFile.length() > getMaxSize(targetedFile.length()) && retry < maxRetry); } catch (Exception e) { e.printStackTrace(); if (decodeBitmapInputStream != null) { try { decodeBitmapInputStream.close(); } catch (IOException ex) { ex.printStackTrace(); } } } } private static int calculateInSampleSize(BitmapFactory.Options options, int rqWidth, int rqHeight) { int height = options.outHeight; int width = options.outWidth; int inSampleSize = 1; if (height > rqHeight || width > rqWidth) { int halfHeight = height / 2; int halfWidth = width / 2; while ((halfHeight / inSampleSize) > rqHeight && (halfWidth / inSampleSize) > rqWidth) { inSampleSize *= 2; } } return inSampleSize; } private static int getImageOrientation(Uri uri, ContentResolver contentResolver) { InputStream inputStream; try { inputStream = contentResolver.openInputStream(uri); } catch (FileNotFoundException e) { e.printStackTrace(); return ExifInterface.ORIENTATION_UNDEFINED; } if (inputStream == null) { return ExifInterface.ORIENTATION_UNDEFINED; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { try { ExifInterface exifInterface = new ExifInterface(inputStream); int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); inputStream.close(); return orientation; } catch (IOException e) { try { inputStream.close(); } catch (IOException ex) { ex.printStackTrace(); return ExifInterface.ORIENTATION_UNDEFINED; } e.printStackTrace(); return ExifInterface.ORIENTATION_UNDEFINED; } } else { try { ExifInterface exifInterface = new ExifInterface(uri.getPath()); int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); inputStream.close(); return orientation; } catch (IOException e) { try { inputStream.close(); } catch (IOException ex) { ex.printStackTrace(); return ExifInterface.ORIENTATION_UNDEFINED; } e.printStackTrace(); return ExifInterface.ORIENTATION_UNDEFINED; } } } private static long getMaxSize(long maxSize) { if (MainActivity.instanceInfo != null && MainActivity.instanceInfo.configuration != null && MainActivity.instanceInfo.configuration.media_attachments != null) { maxSize = MainActivity.instanceInfo.configuration.media_attachments.image_size_limit; } return maxSize; } public static Bitmap reorientBitmap(Bitmap bitmap, int orientation) { Matrix matrix = new Matrix(); switch (orientation) { case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: matrix.setScale(-1.0f, 1.0f); break; case ExifInterface.ORIENTATION_ROTATE_180: matrix.setRotate(180.0f); break; case ExifInterface.ORIENTATION_FLIP_VERTICAL: matrix.setRotate(180.0f); matrix.postScale(-1.0f, 1.0f); break; case ExifInterface.ORIENTATION_TRANSPOSE: matrix.setRotate(90.0f); matrix.postScale(-1.0f, 1.0f); break; case ExifInterface.ORIENTATION_ROTATE_90: matrix.setRotate(90.0f); break; case ExifInterface.ORIENTATION_TRANSVERSE: matrix.setRotate(-90.0f); matrix.postScale(-1.0f, 1.0f); break; case ExifInterface.ORIENTATION_ROTATE_270: matrix.setRotate(-90.0f); break; default: return bitmap; } if (bitmap == null) { return null; } try { Bitmap result = Bitmap.createBitmap( bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); if (!bitmap.sameAs(result)) { bitmap.recycle(); } return result; } catch (Exception e) { e.printStackTrace(); } return null; } //Listener for recording media public interface ActionRecord { void onRecorded(String file); } public interface OnSchedule { void scheduledAt(String scheduledDate); } }