/*####################################################### * * Maintained by Gregor Santner, 2017- * https://gsantner.net/ * * License of this file: Apache 2.0 (Commercial upon request) * https://www.apache.org/licenses/LICENSE-2.0 * https://github.com/gsantner/opoc/#licensing * #########################################################*/ package net.gsantner.opoc.util; import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.Handler; import android.os.ParcelFileDescriptor; import android.print.PrintAttributes; import android.print.PrintDocumentAdapter; import android.print.PrintJob; import android.print.PrintManager; import android.provider.CalendarContract; import android.provider.MediaStore; import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.annotation.StringRes; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v4.content.FileProvider; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.pm.ShortcutInfoCompat; import android.support.v4.content.pm.ShortcutManagerCompat; import android.support.v4.graphics.drawable.IconCompat; import android.support.v4.provider.DocumentFile; import android.support.v4.util.Pair; import android.support.v7.app.AlertDialog; import android.support.v7.preference.PreferenceManager; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.webkit.WebView; import android.widget.ImageView; import android.widget.Toast; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Random; import static android.app.Activity.RESULT_OK; /** * A utility class to ease information sharing on Android. * Also allows to parse/fetch information out of shared information. * (M)Permissions are not checked, wrap ShareUtils methods if neccessary */ @SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "SameParameterValue", "unused", "deprecation", "ConstantConditions", "ObsoleteSdkInt", "SpellCheckingInspection", "JavadocReference", "ConstantLocale"}) public class ShareUtil { public final static String EXTRA_FILEPATH = "real_file_path_2"; public final static SimpleDateFormat SDF_RFC3339_ISH = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss", Locale.getDefault()); public final static SimpleDateFormat SDF_SHORT = new SimpleDateFormat("yyMMdd-HHmmss", Locale.getDefault()); public final static String MIME_TEXT_PLAIN = "text/plain"; public final static String PREF_KEY__SAF_TREE_URI = "pref_key__saf_tree_uri"; public final static int REQUEST_CAMERA_PICTURE = 50001; public final static int REQUEST_PICK_PICTURE = 50002; public final static int REQUEST_SAF = 50003; public final static int MIN_OVERWRITE_LENGTH = 5; protected static String _lastCameraPictureFilepath; protected Context _context; protected String _fileProviderAuthority; protected String _chooserTitle; public ShareUtil(final Context context) { _context = context; _chooserTitle = "➥"; } public void setContext(final Context c) { _context = c; } public void freeContextRef() { _context = null; } public String getFileProviderAuthority() { if (TextUtils.isEmpty(_fileProviderAuthority)) { throw new RuntimeException("Error at ShareUtil.getFileProviderAuthority(): No FileProvider authority provided"); } return _fileProviderAuthority; } public ShareUtil setFileProviderAuthority(final String fileProviderAuthority) { _fileProviderAuthority = fileProviderAuthority; return this; } public ShareUtil setChooserTitle(final String title) { _chooserTitle = title; return this; } /** * Convert a {@link File} to an {@link Uri} * * @param file the file * @return Uri for this file */ public Uri getUriByFileProviderAuthority(final File file) { return FileProvider.getUriForFile(_context, getFileProviderAuthority(), file); } /** * Allow to choose a handling app for given intent * * @param intent Thing to be shared * @param chooserText The title text for the chooser, or null for default */ public void showChooser(final Intent intent, final String chooserText) { try { _context.startActivity(Intent.createChooser(intent, chooserText != null ? chooserText : _chooserTitle)); } catch (Exception ignored) { } } /** * Try to create a new desktop shortcut on the launcher. Add permissions: * * * * @param intent The intent to be invoked on tap * @param iconRes Icon resource for the item * @param title Title of the item */ public void createLauncherDesktopShortcut(final Intent intent, @DrawableRes final int iconRes, final String title) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); if (intent.getAction() == null) { intent.setAction(Intent.ACTION_VIEW); } ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(_context, Long.toString(new Random().nextLong())) .setIntent(intent) .setIcon(IconCompat.createWithResource(_context, iconRes)) .setShortLabel(title) .setLongLabel(title) .build(); ShortcutManagerCompat.requestPinShortcut(_context, shortcut, null); } /** * Try to create a new desktop shortcut on the launcher. This will not work on Api > 25. Add permissions: * * * * @param intent The intent to be invoked on tap * @param iconRes Icon resource for the item * @param title Title of the item */ public void createLauncherDesktopShortcutLegacy(final Intent intent, @DrawableRes final int iconRes, final String title) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); if (intent.getAction() == null) { intent.setAction(Intent.ACTION_VIEW); } Intent creationIntent = new Intent("com.android.launcher.action.INSTALL_SHORTCUT"); creationIntent.putExtra("duplicate", true); creationIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, intent); creationIntent.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); creationIntent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(_context, iconRes)); _context.sendBroadcast(creationIntent); } /** * Share text with given mime-type * * @param text The text to share * @param mimeType MimeType or null (uses text/plain) */ public void shareText(final String text, @Nullable final String mimeType) { Intent intent = new Intent(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_TEXT, text); intent.setType(mimeType != null ? mimeType : MIME_TEXT_PLAIN); showChooser(intent, null); } /** * Share the given file as stream with given mime-type * * @param file The file to share * @param mimeType The files mime type */ public boolean shareStream(final File file, final String mimeType) { Intent intent = new Intent(Intent.ACTION_SEND); intent.putExtra(EXTRA_FILEPATH, file.getAbsolutePath()); intent.setType(mimeType); try { Uri fileUri = FileProvider.getUriForFile(_context, getFileProviderAuthority(), file); intent.putExtra(Intent.EXTRA_STREAM, fileUri); showChooser(intent, null); return true; } catch (Exception e) { // FileUriExposed(API24) / IllegalArgument return false; } } /** * Share the given files as stream with given mime-type * * @param files The files to share * @param mimeType The files mime type. Usally * / * is the best option */ public boolean shareStreamMultiple(final Collection files, final String mimeType) { ArrayList uris = new ArrayList<>(); for (File file : files) { File uri = new File(file.toString()); uris.add(FileProvider.getUriForFile(_context, getFileProviderAuthority(), file)); } try { Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); intent.setType(mimeType); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); showChooser(intent, null); return true; } catch (Exception e) { // FileUriExposed(API24) / IllegalArgument return false; } } /** * Start calendar application to add new event, with given details prefilled */ public boolean createCalendarAppointment(@Nullable final String title, @Nullable final String description, @Nullable final String location, @Nullable final Long... startAndEndTime) { Intent intent = new Intent(Intent.ACTION_INSERT).setData(CalendarContract.Events.CONTENT_URI); if (title != null) { intent.putExtra(CalendarContract.Events.TITLE, title); } if (description != null) { intent.putExtra(CalendarContract.Events.DESCRIPTION, (description.length() > 800 ? description.substring(0, 800) : description)); } if (location != null) { intent.putExtra(CalendarContract.Events.EVENT_LOCATION, location); } if (startAndEndTime != null) { if (startAndEndTime.length > 0 && startAndEndTime[0] > 0) { intent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startAndEndTime[0]); } if (startAndEndTime.length > 1 && startAndEndTime[1] > 0) { intent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, startAndEndTime[1]); } } try { _context.startActivity(intent); return true; } catch (ActivityNotFoundException e) { return false; } } /** * Open a View intent for given file * * @param file The file to share */ public boolean viewFileInOtherApp(final File file, @Nullable final String type) { // On some specific devices the first won't work Uri fileUri = null; try { fileUri = FileProvider.getUriForFile(_context, getFileProviderAuthority(), file); } catch (Exception ignored) { try { fileUri = Uri.fromFile(file); } catch (Exception ignored2) { } } if (fileUri != null) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.putExtra(Intent.EXTRA_STREAM, fileUri); intent.setData(fileUri); intent.putExtra(EXTRA_FILEPATH, file.getAbsolutePath()); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setDataAndType(fileUri, type); showChooser(intent, null); return true; } return false; } /** * Share the given bitmap with given format * * @param bitmap Image * @param format A {@link Bitmap.CompressFormat}, supporting JPEG,PNG,WEBP * @return if success, true */ public boolean shareImage(final Bitmap bitmap, final Bitmap.CompressFormat format) { return shareImage(bitmap, format, 95, "SharedImage"); } /** * Share the given bitmap with given format * * @param bitmap Image * @param format A {@link Bitmap.CompressFormat}, supporting JPEG,PNG,WEBP * @param imageName Filename without extension * @param quality Quality of the exported image [0-100] * @return if success, true */ public boolean shareImage(final Bitmap bitmap, final Bitmap.CompressFormat format, final int quality, final String imageName) { try { String ext = format.name().toLowerCase(); File file = File.createTempFile(imageName, "." + ext.replace("jpeg", "jpg"), _context.getExternalCacheDir()); if (bitmap != null && new ContextUtils(_context).writeImageToFile(file, bitmap, format, quality)) { shareStream(file, "image/" + ext); return true; } } catch (IOException e) { e.printStackTrace(); } return false; } /** * Print a {@link WebView}'s contents, also allows to create a PDF * * @param webview WebView * @param jobName Name of the job (affects PDF name too) * @return {{@link PrintJob}} or null */ @RequiresApi(api = Build.VERSION_CODES.KITKAT) public PrintJob print(final WebView webview, final String jobName, final boolean... landscape) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { final PrintDocumentAdapter printAdapter; final PrintManager printManager = (PrintManager) _context.getSystemService(Context.PRINT_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { printAdapter = webview.createPrintDocumentAdapter(jobName); } else { printAdapter = webview.createPrintDocumentAdapter(); } final PrintAttributes.Builder attrib = new PrintAttributes.Builder(); if (landscape != null && landscape.length > 0 && landscape[0]) { attrib.setMediaSize(new PrintAttributes.MediaSize("ISO_A4", "android", 11690, 8270)); attrib.setMinMargins(new PrintAttributes.Margins(0, 0, 0, 0)); } if (printManager != null) { try { return printManager.print(jobName, printAdapter, attrib.build()); } catch (Exception ignored) { } } } else { Log.e(getClass().getName(), "ERROR: Method called on too low Android API version"); } return null; } /** * See {@link #print(WebView, String) print method} */ @RequiresApi(api = Build.VERSION_CODES.KITKAT) public PrintJob createPdf(final WebView webview, final String jobName) { return print(webview, jobName); } /** * Create a picture out of {@link WebView}'s whole content * * @param webView The WebView to get contents from * @return A {@link Bitmap} or null */ @Nullable public static Bitmap getBitmapFromWebView(final WebView webView) { try { //Measure WebView's content int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); webView.measure(widthMeasureSpec, heightMeasureSpec); webView.layout(0, 0, webView.getMeasuredWidth(), webView.getMeasuredHeight()); //Build drawing cache and store its size webView.buildDrawingCache(); int measuredWidth = webView.getMeasuredWidth(); int measuredHeight = webView.getMeasuredHeight(); //Creates the bitmap and draw WebView's content on in Bitmap bitmap = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); canvas.drawBitmap(bitmap, 0, bitmap.getHeight(), new Paint()); webView.draw(canvas); webView.destroyDrawingCache(); return bitmap; } catch (Exception | OutOfMemoryError e) { e.printStackTrace(); return null; } } /*** * Replace (primary) clipboard contents with given {@code text} * @param text Text to be set */ public boolean setClipboard(final CharSequence text) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { android.text.ClipboardManager cm = ((android.text.ClipboardManager) _context.getSystemService(Context.CLIPBOARD_SERVICE)); if (cm != null) { cm.setText(text); return true; } } else { android.content.ClipboardManager cm = ((android.content.ClipboardManager) _context.getSystemService(Context.CLIPBOARD_SERVICE)); if (cm != null) { ClipData clip = ClipData.newPlainText(_context.getPackageName(), text); try { cm.setPrimaryClip(clip); } catch (Exception ignored) { } return true; } } return false; } /** * Get clipboard contents, very failsafe and compat to older android versions */ public List getClipboard() { List clipper = new ArrayList<>(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { android.text.ClipboardManager cm = ((android.text.ClipboardManager) _context.getSystemService(Context.CLIPBOARD_SERVICE)); if (cm != null && !TextUtils.isEmpty(cm.getText())) { clipper.add(cm.getText().toString()); } } else { android.content.ClipboardManager cm = ((android.content.ClipboardManager) _context.getSystemService(Context.CLIPBOARD_SERVICE)); if (cm != null && cm.hasPrimaryClip()) { ClipData data = cm.getPrimaryClip(); for (int i = 0; data != null && i < data.getItemCount() && i < data.getItemCount(); i++) { ClipData.Item item = data.getItemAt(i); if (item != null && !TextUtils.isEmpty(item.getText())) { clipper.add(data.getItemAt(i).getText().toString()); } } } } return clipper; } /** * Share given text on a hastebin compatible server * (https://github.com/seejohnrun/haste-server) * Permission needed: Internet * Pastes will be deleted after 30 days without access * * @param text The text to paste * @param callback Callback after paste try * @param serverOrNothing Supply one or no hastebin server. If empty, the default gets taken */ public void pasteOnHastebin(final String text, final Callback.a2 callback, final String... serverOrNothing) { final Handler handler = new Handler(); final String server = (serverOrNothing != null && serverOrNothing.length > 0 && serverOrNothing[0] != null) ? serverOrNothing[0] : "https://hastebin.com"; new Thread() { public void run() { // Returns a simple result, handleable without json parser {"key":"feediyujiq"} String ret = NetworkUtils.performCall(server + "/documents", NetworkUtils.POST, text); final String key = (ret.length() > 15) ? ret.split("\"")[3] : ""; handler.post(() -> callback.callback(!key.isEmpty(), server + "/" + key)); } }.start(); } /** * Draft an email with given data. Unknown data can be supplied as null. * This will open a chooser with installed mail clients where the mail can be sent from * * @param subject Subject (top/title) text to be prefilled in the mail * @param body Body (content) text to be prefilled in the mail * @param to recipients to be prefilled in the mail */ public void draftEmail(final String subject, final String body, final String... to) { Intent intent = new Intent(Intent.ACTION_SENDTO); intent.setData(Uri.parse("mailto:")); if (subject != null) { intent.putExtra(Intent.EXTRA_SUBJECT, subject); } if (body != null) { intent.putExtra(Intent.EXTRA_TEXT, body); } if (to != null && to.length > 0 && to[0] != null) { intent.putExtra(Intent.EXTRA_EMAIL, to); } showChooser(intent, null); } /** * Try to force extract a absolute filepath from an intent * * @param receivingIntent The intent from {@link Activity#getIntent()} * @return A file or null if extraction did not succeed */ public File extractFileFromIntent(final Intent receivingIntent) { String action = receivingIntent.getAction(); String type = receivingIntent.getType(); File tmpf; String tmps; String fileStr; if ((Intent.ACTION_VIEW.equals(action) || Intent.ACTION_EDIT.equals(action)) || Intent.ACTION_SEND.equals(action)) { // Markor, S.M.T FileManager if (receivingIntent.hasExtra((tmps = EXTRA_FILEPATH))) { return new File(receivingIntent.getStringExtra(tmps)); } // Analyze data/Uri Uri fileUri = receivingIntent.getData(); if (fileUri != null && (fileStr = fileUri.toString()) != null) { // Uri contains file if (fileStr.startsWith("file://")) { return new File(fileUri.getPath()); } if (fileStr.startsWith((tmps = "content://"))) { fileStr = fileStr.substring(tmps.length()); String fileProvider = fileStr.substring(0, fileStr.indexOf("/")); fileStr = fileStr.substring(fileProvider.length() + 1); // Some file managers dont add leading slash if (fileStr.startsWith("storage/")) { fileStr = "/" + fileStr; } // Some do add some custom prefix for (String prefix : new String[]{"file", "document", "root_files", "name"}) { if (fileStr.startsWith(prefix)) { fileStr = fileStr.substring(prefix.length()); } } // prefix for External storage (/storage/emulated/0 /// /sdcard/) --> e.g. "content://com.amaze.filemanager/storage_root/file.txt" = "/sdcard/file.txt" for (String prefix : new String[]{"external/", "media/", "storage_root/"}) { if (fileStr.startsWith((tmps = prefix))) { File f = new File(Uri.decode(Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + fileStr.substring(tmps.length()))); if (f.exists()) { return f; } } } // Next/OwnCloud Fileprovider for (String fp : new String[]{"org.nextcloud.files", "org.nextcloud.beta.files", "org.owncloud.files"}) { if (fileProvider.equals(fp) && fileStr.startsWith(tmps = "external_files/")) { return new File(Uri.decode("/storage/" + fileStr.substring(tmps.length()))); } } // AOSP File Manager/Documents if (fileProvider.equals("com.android.externalstorage.documents") && fileStr.startsWith(tmps = "/primary%3A")) { return new File(Uri.decode(Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + fileStr.substring(tmps.length()))); } // Mi File Explorer if (fileProvider.equals("com.mi.android.globalFileexplorer.myprovider") && fileStr.startsWith(tmps = "external_files")) { return new File(Uri.decode(Environment.getExternalStorageDirectory().getAbsolutePath() + fileStr.substring(tmps.length()))); } if (fileStr.startsWith(tmps = "external_files/")) { for (String prefix : new String[]{Environment.getExternalStorageDirectory().getAbsolutePath(), "/storage", ""}) { File f = new File(Uri.decode(prefix + "/" + fileStr.substring(tmps.length()))); if (f.exists()) { return f; } } } // URI Encoded paths with full path after content://package/ if (fileStr.startsWith("/") || fileStr.startsWith("%2F")) { tmpf = new File(Uri.decode(fileStr)); if (tmpf.exists()) { return tmpf; } else if ((tmpf = new File(fileStr)).exists()) { return tmpf; } } } } fileUri = receivingIntent.getParcelableExtra(Intent.EXTRA_STREAM); if (fileUri != null && !TextUtils.isEmpty(tmps = fileUri.getPath()) && tmps.startsWith("/") && (tmpf = new File(tmps)).exists()) { return tmpf; } } return null; } /** * Request a picture from gallery * Result will be available from {@link Activity#onActivityResult(int, int, Intent)}. * It will return the path to the image if locally stored. If retrieved from e.g. a cloud * service, the image will get copied to app-cache folder and it's path returned. */ public void requestGalleryPicture() { if (!(_context instanceof Activity)) { throw new RuntimeException("Error: ShareUtil.requestGalleryPicture needs an Activity Context."); } Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); try { ((Activity) _context).startActivityForResult(intent, REQUEST_PICK_PICTURE); } catch (Exception ex) { Toast.makeText(_context, "No gallery app installed!", Toast.LENGTH_SHORT).show(); } } public String extractFileFromIntentStr(final Intent receivingIntent) { File f = extractFileFromIntent(receivingIntent); return f != null ? f.getAbsolutePath() : null; } /** * Request a picture from camera-like apps * Result ({@link String}) will be available from {@link Activity#onActivityResult(int, int, Intent)}. * It has set resultCode to {@link Activity#RESULT_OK} with same requestCode, if successfully * The requested image savepath has to be stored at caller side (not contained in intent), * it can be retrieved using {@link #extractResultFromActivityResult(int, int, Intent, Activity...)} * returns null if an error happened. * * @param target Path to file to write to, if folder the filename gets app_name + millis + random filename. If null DCIM folder is used. */ @SuppressWarnings("RegExpRedundantEscape") public String requestCameraPicture(final File target) { if (!(_context instanceof Activity)) { throw new RuntimeException("Error: ShareUtil.requestCameraPicture needs an Activity Context."); } String cameraPictureFilepath = null; Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (takePictureIntent.resolveActivity(_context.getPackageManager()) != null) { File photoFile; try { // Create an image file name if (target != null && !target.isDirectory()) { photoFile = target; } else { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss", Locale.ENGLISH); File storageDir = target != null ? target : new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), "Camera"); String imageFileName = ((new ContextUtils(_context).rstr("app_name")).replaceAll("[^a-zA-Z0-9\\.\\-]", "_") + "_").replace("__", "_") + sdf.format(new Date()); photoFile = new File(storageDir, imageFileName + ".jpg"); if (!photoFile.getParentFile().exists() && !photoFile.getParentFile().mkdirs()) { photoFile = File.createTempFile(imageFileName + "_", ".jpg", storageDir); } } //noinspection StatementWithEmptyBody if (!photoFile.getParentFile().exists() && photoFile.getParentFile().mkdirs()) ; // Save a file: path for use with ACTION_VIEW intents cameraPictureFilepath = photoFile.getAbsolutePath(); } catch (IOException ex) { return null; } // Continue only if the File was successfully created if (photoFile != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Uri uri = FileProvider.getUriForFile(_context, getFileProviderAuthority(), photoFile); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri); } else { takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile)); } ((Activity) _context).startActivityForResult(takePictureIntent, REQUEST_CAMERA_PICTURE); } } _lastCameraPictureFilepath = cameraPictureFilepath; return cameraPictureFilepath; } /** * Extract result data from {@link Activity#onActivityResult(int, int, Intent)}. * Forward all arguments from activity. Only requestCodes from {@link ShareUtil} get analyzed. * Also may forward results via local broadcast */ @SuppressLint("ApplySharedPref") public Object extractResultFromActivityResult(final int requestCode, final int resultCode, final Intent data, final Activity... activityOrNull) { Activity activity = greedyGetActivity(activityOrNull); switch (requestCode) { case REQUEST_CAMERA_PICTURE: { String picturePath = (resultCode == RESULT_OK) ? _lastCameraPictureFilepath : null; if (picturePath != null) { sendLocalBroadcastWithStringExtra(REQUEST_CAMERA_PICTURE + "", EXTRA_FILEPATH, picturePath); } return picturePath; } case REQUEST_PICK_PICTURE: { if (resultCode == RESULT_OK && data != null) { Uri selectedImage = data.getData(); String[] filePathColumn = {MediaStore.Images.Media.DATA}; String picturePath = null; Cursor cursor = _context.getContentResolver().query(selectedImage, filePathColumn, null, null, null); if (cursor != null && cursor.moveToFirst()) { for (String column : filePathColumn) { int curColIndex = cursor.getColumnIndex(column); if (curColIndex == -1) { continue; } picturePath = cursor.getString(curColIndex); if (!TextUtils.isEmpty(picturePath)) { break; } } cursor.close(); } // Try to grab via file extraction method data.setAction(Intent.ACTION_VIEW); picturePath = picturePath != null ? picturePath : extractFileFromIntentStr(data); // Retrieve image from file descriptor / Cloud, e.g.: Google Drive, Picasa if (picturePath == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { try { ParcelFileDescriptor parcelFileDescriptor = _context.getContentResolver().openFileDescriptor(selectedImage, "r"); if (parcelFileDescriptor != null) { FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor(); FileInputStream input = new FileInputStream(fileDescriptor); // Create temporary file in cache directory picturePath = File.createTempFile("image", "tmp", _context.getCacheDir()).getAbsolutePath(); FileUtils.writeFile(new File(picturePath), FileUtils.readCloseBinaryStream(input)); } } catch (IOException ignored) { // nothing we can do here, null value will be handled below } } // Return path to picture on success, else null if (picturePath != null) { sendLocalBroadcastWithStringExtra(REQUEST_CAMERA_PICTURE + "", EXTRA_FILEPATH, picturePath); } return picturePath; } break; } case REQUEST_SAF: { if (resultCode == RESULT_OK && data != null && data.getData() != null) { Uri treeUri = data.getData(); PreferenceManager.getDefaultSharedPreferences(_context).edit().putString(PREF_KEY__SAF_TREE_URI, treeUri.toString()).commit(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { activity.getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } return treeUri; } break; } } return null; } /** * Send a local broadcast (to receive within app), with given action and string-extra+value. * This is a convenience method for quickly sending just one thing. */ public void sendLocalBroadcastWithStringExtra(final String action, final String extra, final CharSequence value) { Intent intent = new Intent(action); intent.putExtra(extra, value); LocalBroadcastManager.getInstance(_context).sendBroadcast(intent); } /** * Receive broadcast results via a callback method * * @param callback Function to call with received {@link Intent} * @param autoUnregister wether or not to automatically unregister receiver after first match * @param filterActions All {@link IntentFilter} actions to filter for * @return The created instance. Has to be unregistered on {@link Activity} lifecycle events. */ public BroadcastReceiver receiveResultFromLocalBroadcast(final Callback.a2 callback, final boolean autoUnregister, final String... filterActions) { IntentFilter intentFilter = new IntentFilter(); for (String filterAction : filterActions) { intentFilter.addAction(filterAction); } final BroadcastReceiver br = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent != null) { if (autoUnregister) { LocalBroadcastManager.getInstance(_context).unregisterReceiver(this); } try { callback.callback(intent, this); } catch (Exception ignored) { } } } }; LocalBroadcastManager.getInstance(_context).registerReceiver(br, intentFilter); return br; } /** * Request edit of image (by image editor/viewer - for example to crop image) * * @param file File that should be edited */ public void requestPictureEdit(final File file) { Uri uri = getUriByFileProviderAuthority(file); int flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION; Intent intent = new Intent(Intent.ACTION_EDIT); intent.setDataAndType(uri, "image/*"); intent.addFlags(flags); intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); intent.putExtra(EXTRA_FILEPATH, file.getAbsolutePath()); for (ResolveInfo resolveInfo : _context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)) { String packageName = resolveInfo.activityInfo.packageName; _context.grantUriPermission(packageName, uri, flags); } _context.startActivity(Intent.createChooser(intent, null)); } /** * Get content://media/ Uri for given file, or null if not indexed * * @param file Target file * @param mode 1 for picture, 2 for video, anything else for other * @return Media URI */ @SuppressWarnings("TryFinallyCanBeTryWithResources") public Uri getMediaUri(final File file, final int mode) { Uri uri = MediaStore.Files.getContentUri("external"); uri = (mode != 0) ? (mode == 1 ? MediaStore.Images.Media.EXTERNAL_CONTENT_URI : MediaStore.Video.Media.EXTERNAL_CONTENT_URI) : uri; Cursor cursor = null; try { cursor = _context.getContentResolver().query(uri, new String[]{MediaStore.Images.Media._ID}, MediaStore.Images.Media.DATA + "= ?", new String[]{file.getAbsolutePath()}, null); if (cursor != null && cursor.moveToFirst()) { int mediaid = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID)); return Uri.withAppendedPath(uri, mediaid + ""); } } catch (Exception ignored) { } finally { if (cursor != null) { cursor.close(); } } return null; } /** * By default Chrome Custom Tabs only uses Chrome Stable to open links * There are also other packages (like Chrome Beta, Chromium, Firefox, ..) * which implement the Chrome Custom Tab interface. This method changes * the customtab intent to use an available compatible browser, if available. */ public void enableChromeCustomTabsForOtherBrowsers(final Intent customTabIntent) { String[] checkpkgs = new String[]{ "com.android.chrome", "com.chrome.beta", "com.chrome.dev", "com.google.android.apps.chrome", "org.chromium.chrome", "org.mozilla.fennec_fdroid", "org.mozilla.firefox", "org.mozilla.firefox_beta", "org.mozilla.fennec_aurora", "org.mozilla.klar", "org.mozilla.focus", }; // Get all intent handlers for web links PackageManager pm = _context.getPackageManager(); Intent urlIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.example.com")); List browsers = new ArrayList<>(); for (ResolveInfo ri : pm.queryIntentActivities(urlIntent, 0)) { Intent i = new Intent("android.support.customtabs.action.CustomTabsService"); i.setPackage(ri.activityInfo.packageName); if (pm.resolveService(i, 0) != null) { browsers.add(ri.activityInfo.packageName); } } // Check if the user has a "default browser" selected ResolveInfo ri = pm.resolveActivity(urlIntent, 0); String userDefaultBrowser = (ri == null) ? null : ri.activityInfo.packageName; // Select which browser to use out of all installed customtab supporting browsers String pkg = null; if (browsers.isEmpty()) { pkg = null; } else if (browsers.size() == 1) { pkg = browsers.get(0); } else if (!TextUtils.isEmpty(userDefaultBrowser) && browsers.contains(userDefaultBrowser)) { pkg = userDefaultBrowser; } else { for (String checkpkg : checkpkgs) { if (browsers.contains(checkpkg)) { pkg = checkpkg; break; } } if (pkg == null && !browsers.isEmpty()) { pkg = browsers.get(0); } } if (pkg != null && customTabIntent != null) { customTabIntent.setPackage(pkg); } } /*** * Request storage access. The user needs to press "Select storage" at the correct storage. * @param activity The activity which will receive the result from startActivityForResult */ public void requestStorageAccessFramework(final Activity... activity) { Activity a = greedyGetActivity(activity); if (a != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION ); a.startActivityForResult(intent, REQUEST_SAF); } } /** * Get storage access framework tree uri. The user must have granted access via {@link #requestStorageAccessFramework(Activity...)} * * @return Uri or null if not granted yet */ public Uri getStorageAccessFrameworkTreeUri() { String treeStr = PreferenceManager.getDefaultSharedPreferences(_context).getString(PREF_KEY__SAF_TREE_URI, null); if (!TextUtils.isEmpty(treeStr)) { try { return Uri.parse(treeStr); } catch (Exception ignored) { } } return null; } /** * Get mounted storage folder root (by tree uri). The user must have granted access via {@link #requestStorageAccessFramework(Activity...)} * * @return File or null if SD not mounted */ public File getStorageAccessFolder() { Uri safUri = getStorageAccessFrameworkTreeUri(); if (safUri != null) { String safUriStr = safUri.toString(); ContextUtils cu = new ContextUtils(_context); for (Pair storage : cu.getStorages(false, true)) { @SuppressWarnings("ConstantConditions") String storageFolderName = storage.first.getName(); if (safUriStr.contains(storageFolderName)) { return storage.first; } } cu.freeContextRef(); } return null; } /** * Check whether or not a file is under a storage access folder (external storage / SD) * * @param file The file object (file/folder) * @return Wether or not the file is under storage access folder */ public boolean isUnderStorageAccessFolder(final File file) { if (file != null) { // When file writeable as is, it's the fastest way to learn SAF isn't required if (file.canWrite()) { return false; } ContextUtils cu = new ContextUtils(_context); for (Pair storage : cu.getStorages(false, true)) { if (file.getAbsolutePath().startsWith(storage.first.getAbsolutePath())) { cu.freeContextRef(); return true; } } cu.freeContextRef(); } return false; } /** * Greedy extract Activity from parameter or convert context if it's a activity */ private Activity greedyGetActivity(final Activity... activity) { if (activity != null && activity.length != 0 && activity[0] != null) { return activity[0]; } if (_context instanceof Activity) { return (Activity) _context; } return null; } /** * Check whether or not a file can be written. * Requires storage access framework permission for external storage (SD) * * @param file The file object (file/folder) * @param isDir Wether or not the given file parameter is a directory * @return Wether or not the file can be written */ public boolean canWriteFile(final File file, final boolean isDir) { if (file == null) { return false; } else if (file.getAbsolutePath().startsWith(Environment.getExternalStorageDirectory().getAbsolutePath()) || file.getAbsolutePath().startsWith(_context.getFilesDir().getAbsolutePath())) { boolean s1 = isDir && file.getParentFile().canWrite(); return !isDir && file.getParentFile() != null ? file.getParentFile().canWrite() : file.canWrite(); } else { DocumentFile dof = getDocumentFile(file, isDir); return dof != null && dof.canWrite(); } } /** * Get a {@link DocumentFile} object out of a normal java {@link File}. * When used on a external storage (SD), use {@link #requestStorageAccessFramework(Activity...)} * first to get access. Otherwise this will fail. * * @param file The file/folder to convert * @param isDir Wether or not file is a directory. For non-existing (to be created) files this info is not known hence required. * @return A {@link DocumentFile} object or null if file cannot be converted */ @SuppressWarnings("RegExpRedundantEscape") public DocumentFile getDocumentFile(final File file, final boolean isDir) { // On older versions use fromFile if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { return DocumentFile.fromFile(file); } // Get ContextUtils to find storageRootFolder ContextUtils cu = new ContextUtils(_context); File baseFolderFile = cu.getStorageRootFolder(file); cu.freeContextRef(); String baseFolder = baseFolderFile == null ? null : baseFolderFile.getAbsolutePath(); boolean originalDirectory = false; if (baseFolder == null) { return null; } String relPath = null; try { String fullPath = file.getCanonicalPath(); if (!baseFolder.equals(fullPath)) { relPath = fullPath.substring(baseFolder.length() + 1); } else { originalDirectory = true; } } catch (IOException e) { return null; } catch (Exception ignored) { originalDirectory = true; } Uri treeUri; if ((treeUri = getStorageAccessFrameworkTreeUri()) == null) { return null; } DocumentFile dof = DocumentFile.fromTreeUri(_context, treeUri); if (originalDirectory) { return dof; } String[] parts = relPath.split("\\/"); for (int i = 0; i < parts.length; i++) { DocumentFile nextDof = dof.findFile(parts[i]); if (nextDof == null) { try { nextDof = ((i < parts.length - 1) || isDir) ? dof.createDirectory(parts[i]) : dof.createFile("image", parts[i]); } catch (Exception ignored) { nextDof = null; } } dof = nextDof; } return dof; } public void showMountSdDialog(@StringRes final int title, @StringRes final int description, @DrawableRes final int mountDescriptionGraphic, final Activity... activityOrNull) { Activity activity = greedyGetActivity(activityOrNull); if (activity == null) { return; } // Image viewer ImageView imv = new ImageView(activity); imv.setImageResource(mountDescriptionGraphic); imv.setAdjustViewBounds(true); AlertDialog.Builder dialog = new AlertDialog.Builder(activity); dialog.setView(imv); dialog.setTitle(title); dialog.setMessage(_context.getString(description) + "\n\n"); dialog.setNegativeButton(android.R.string.cancel, null); dialog.setPositiveButton(android.R.string.yes, (dialogInterface, i) -> requestStorageAccessFramework(activity)); AlertDialog dialogi = dialog.create(); dialogi.show(); } @SuppressWarnings({"ResultOfMethodCallIgnored", "StatementWithEmptyBody"}) public void writeFile(final File file, final boolean isDirectory, final Callback.a2 writeFileCallback) { try { FileOutputStream fileOutputStream = null; ParcelFileDescriptor pfd = null; final boolean existingEmptyFile = file.canWrite() && file.length() < MIN_OVERWRITE_LENGTH; final boolean nonExistingCreatableFile = !file.exists() && file.getParentFile().canWrite(); if (existingEmptyFile || nonExistingCreatableFile) { if (isDirectory) { file.mkdirs(); } else { fileOutputStream = new FileOutputStream(file); } } else { DocumentFile dof = getDocumentFile(file, isDirectory); if (dof != null && dof.getUri() != null && dof.canWrite()) { if (isDirectory) { // Nothing to do } else { pfd = _context.getContentResolver().openFileDescriptor(dof.getUri(), "rw"); fileOutputStream = new FileOutputStream(pfd.getFileDescriptor()); } } } if (writeFileCallback != null) { writeFileCallback.callback(fileOutputStream != null || (isDirectory && file.exists()), fileOutputStream); } if (fileOutputStream != null) { try { fileOutputStream.close(); } catch (Exception ignored) { } } if (pfd != null) { pfd.close(); } } catch (Exception e) { e.printStackTrace(); } } /** * Call telephone number. * Non direct call, opens up the dialer and pre-sets the telephone number. User needs to press manually. * Direct call requires M permission granted, also add permissions to manifest: * * * @param telNo The telephone number to call * @param directCall Direct call number if possible */ @SuppressWarnings("SimplifiableConditionalExpression") public void callTelephoneNumber(final String telNo, final boolean... directCall) { Activity activity = greedyGetActivity(); if (activity == null) { throw new RuntimeException("Error: ShareUtil::callTelephoneNumber needs to be contstructed with activity context"); } boolean ldirectCall = (directCall != null && directCall.length > 0) ? directCall[0] : true; if (android.os.Build.VERSION.SDK_INT >= 23 && ldirectCall && activity != null) { if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.CALL_PHONE}, 4001); ldirectCall = false; } else { try { Intent callIntent = new Intent(Intent.ACTION_CALL); callIntent.setData(Uri.parse("tel:" + telNo)); activity.startActivity(callIntent); } catch (Exception ignored) { ldirectCall = false; } } } // Show dialer up with telephone number pre-inserted if (!ldirectCall) { Intent intent = new Intent(Intent.ACTION_DIAL, Uri.fromParts("tel", telNo, null)); activity.startActivity(intent); } } }