diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 916459b2..91d95680 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,8 @@ + + = Build.VERSION_CODES.KITKAT) { + menu.findItem(R.id.action_share_pdf).setVisible(true); + } final boolean darkBg = ContextUtils.get().shouldColorOnTopBeLight(AppSettings.get().getPrimaryColor()); ContextUtils.get().tintMenuItems(menu, true, ContextCompat.getColor(getActivity(), darkBg ? R.color.white : R.color.black)); @@ -118,6 +128,8 @@ public class DiasporaStreamFragment extends BrowserFragment { @Override public boolean onOptionsItemSelected(MenuItem item) { AppLog.d(this, "StreamFragment.onOptionsItemSelected()"); + ShareUtil shu = new ShareUtil(getContext()).setFileProviderAuthority(BuildConfig.APPLICATION_ID); + PermissionChecker permc = new PermissionChecker(getActivity()); switch (item.getItemId()) { case R.id.action_reload: { if (WebHelper.isOnline(getContext())) { @@ -144,13 +156,47 @@ public class DiasporaStreamFragment extends BrowserFragment { return true; } + case R.id.action_share_pdf: { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + shu.createPdf(webView, "dandelion-" + ShareUtil.SDF_SHORT.format(new Date())); + } + return true; + } + + case R.id.action_share_link_to_clipboard: { + shu.setClipboard(webView.getUrl()); + Toast.makeText(getContext(), R.string.share__toast_link_address_copied, Toast.LENGTH_SHORT).show(); + return true; + } + + case R.id.action_create_launcher_shortcut: { + if (webView.getUrl() != null) { + Intent intent = new Intent(getContext(), MainActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.setData(Uri.parse(webView.getUrl())); + shu.createLauncherDesktopShortcut(intent, R.drawable.ic_launcher, webView.getTitle()); + } + return true; + } + case R.id.action_take_screenshot: { - makeScreenshotOfWebView(false); + if (permc.doIfExtStoragePermissionGranted(getString(R.string.permissions_screenshot))) { + File fileSaveDirectory = appSettings.getAppSaveDirectory(); + if (permc.mkdirIfStoragePermissionGranted(fileSaveDirectory)) { + Bitmap bmp = ShareUtil.getBitmapFromWebView(webView); + String filename = "dandelion-" + ShareUtil.SDF_SHORT.format(new Date()) + ".jpg"; + _cu.writeImageToFileJpeg(new File(fileSaveDirectory, filename), bmp); + Snackbar.make(webView, getString(R.string.share__toast_screenshot) + + " " + filename, Snackbar.LENGTH_LONG).show(); + } + } return true; } case R.id.action_share_screenshot: { - makeScreenshotOfWebView(true); + if (permc.doIfExtStoragePermissionGranted(getString(R.string.permissions_screenshot))) { + shu.shareImage(ShareUtil.getBitmapFromWebView(webView), Bitmap.CompressFormat.JPEG); + } return true; } } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaPodList.java b/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaPodList.java index 420d4015..3c56cef3 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaPodList.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/data/DiasporaPodList.java @@ -315,8 +315,8 @@ public class DiasporaPodList implements Iterable, S } /* - * Getter & Setter - */ + * Getter & Setter + */ public List getPodUrls() { return _podUrls; } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/util/ActivityUtils.java b/app/src/main/java/com/github/dfa/diaspora_android/util/ActivityUtils.java index 81dbc5c8..817b8e10 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/util/ActivityUtils.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/ActivityUtils.java @@ -81,7 +81,6 @@ public class ActivityUtils extends net.gsantner.opoc.util.ActivityUtils { * @return */ public static Uri getFileSharingUri(Context context, File file) { - return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID, file); } } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/util/AppSettings.java b/app/src/main/java/com/github/dfa/diaspora_android/util/AppSettings.java index 5240e6e8..79953d7a 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/util/AppSettings.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/util/AppSettings.java @@ -18,6 +18,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Color; +import android.os.Environment; import com.github.dfa.diaspora_android.App; import com.github.dfa.diaspora_android.BuildConfig; @@ -31,6 +32,7 @@ import net.gsantner.opoc.preference.SharedPreferencesPropertyBackend; import org.json.JSONException; import org.json.JSONObject; +import java.io.File; import java.util.List; /** @@ -375,6 +377,11 @@ public class AppSettings extends SharedPreferencesPropertyBackend { return value != BuildConfig.VERSION_CODE && !BuildConfig.IS_TEST_BUILD; } + public File getAppSaveDirectory() { + return new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/dandelion"); + + } + public long getLastVisitedPositionInStream() { return getLong(R.string.pref_key__podprofile_last_stream_position, -1, _prefPod); } diff --git a/app/src/main/java/com/github/dfa/diaspora_android/web/BrowserFragment.java b/app/src/main/java/com/github/dfa/diaspora_android/web/BrowserFragment.java index 538523ed..28db279b 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/web/BrowserFragment.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/BrowserFragment.java @@ -18,18 +18,9 @@ */ package com.github.dfa.diaspora_android.web; -import android.Manifest; import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; import android.content.MutableContextWrapper; -import android.content.pm.PackageManager; -import android.graphics.Bitmap; -import android.net.Uri; import android.os.Bundle; -import android.os.Environment; -import android.support.design.widget.Snackbar; -import android.support.v7.app.AlertDialog; import android.view.View; import android.view.ViewGroup; import android.webkit.WebSettings; @@ -38,22 +29,11 @@ import android.widget.ProgressBar; import com.github.dfa.diaspora_android.App; import com.github.dfa.diaspora_android.R; -import com.github.dfa.diaspora_android.activity.MainActivity; import com.github.dfa.diaspora_android.ui.theme.ThemeHelper; import com.github.dfa.diaspora_android.ui.theme.ThemedFragment; -import com.github.dfa.diaspora_android.util.ActivityUtils; import com.github.dfa.diaspora_android.util.AppLog; import com.github.dfa.diaspora_android.util.AppSettings; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - /** * Fragment with a webView and a ProgressBar. * This Fragment retains its instance. @@ -155,96 +135,6 @@ public class BrowserFragment extends ThemedFragment { } } - @SuppressWarnings("ResultOfMethodCallIgnored") - protected boolean makeScreenshotOfWebView(boolean hasToShareScreenshot) { - AppLog.i(this, "StreamFragment.makeScreenshotOfWebView()"); - if (android.os.Build.VERSION.SDK_INT >= 23) { - int hasWRITE_EXTERNAL_STORAGE = getActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE); - if (hasWRITE_EXTERNAL_STORAGE != PackageManager.PERMISSION_GRANTED) { - if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - new AlertDialog.Builder(getContext()) - .setMessage(R.string.permissions_screenshot) - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (android.os.Build.VERSION.SDK_INT >= 23) - requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - MainActivity.REQUEST_CODE_ASK_PERMISSIONS); - } - }) - .show(); - return false; - } - requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, - MainActivity.REQUEST_CODE_ASK_PERMISSIONS); - return false; - } - } - - Date dateNow = new Date(); - DateFormat dateFormat = new SimpleDateFormat("yy_MM_dd--HH_mm_ss", Locale.getDefault()); - File fileSaveDirectory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/Diaspora"); - - String fileSaveName = hasToShareScreenshot ? ".DfA_share.jpg" : String.format("DfA_%s.jpg", dateFormat.format(dateNow)); - if (!fileSaveDirectory.exists()) { - if (!fileSaveDirectory.mkdirs()) { - AppLog.w(this, "Could not mkdir " + fileSaveDirectory.getAbsolutePath()); - } - } - - if (!hasToShareScreenshot) { - Snackbar.make(webView, getString(R.string.share__toast_screenshot) + " " + fileSaveName, Snackbar.LENGTH_LONG).show(); - } - - Bitmap bitmap; - webView.setDrawingCacheEnabled(true); - bitmap = Bitmap.createBitmap(webView.getDrawingCache()); - webView.setDrawingCacheEnabled(false); - - OutputStream bitmapWriter = null; - try { - bitmapWriter = new FileOutputStream(new File(fileSaveDirectory, fileSaveName)); - bitmap.compress(Bitmap.CompressFormat.JPEG, 85, bitmapWriter); - bitmapWriter.flush(); - bitmap.recycle(); - } catch (Exception e) { - return false; - } finally { - if (bitmapWriter != null) { - try { - bitmapWriter.close(); - } catch (IOException _ignSaveored) {/* Nothing */} - } - } - - // Only show share intent when Action Share Screenshot was selected - if (hasToShareScreenshot) { - - Uri bmpUri = ActivityUtils.getFileSharingUri(getContext(), new File(fileSaveDirectory, fileSaveName)); - - Intent sharingIntent = new Intent(Intent.ACTION_SEND); - sharingIntent.setType("image/jpeg"); - sharingIntent.putExtra(Intent.EXTRA_SUBJECT, webView.getTitle()); - sharingIntent.putExtra(Intent.EXTRA_TEXT, webView.getUrl()); - sharingIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - sharingIntent.putExtra(Intent.EXTRA_STREAM, bmpUri); - - PackageManager pm = getActivity().getPackageManager(); - - if (sharingIntent.resolveActivity(pm) != null) { - startActivity(Intent.createChooser(sharingIntent, getString(R.string.action_share_dotdotdot))); - } - } else { - // Broadcast that this file is indexable - File file = new File(fileSaveDirectory, fileSaveName); - Uri uri = Uri.fromFile(file); - Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri); - getActivity().sendBroadcast(intent); - } - return true; - } - @Override public String getFragmentTag() { return TAG; diff --git a/app/src/main/java/com/github/dfa/diaspora_android/web/ContextMenuWebView.java b/app/src/main/java/com/github/dfa/diaspora_android/web/ContextMenuWebView.java index d450a51f..9e10a218 100644 --- a/app/src/main/java/com/github/dfa/diaspora_android/web/ContextMenuWebView.java +++ b/app/src/main/java/com/github/dfa/diaspora_android/web/ContextMenuWebView.java @@ -230,7 +230,7 @@ public class ContextMenuWebView extends NestedWebView { result.getType() == HitTestResult.SRC_ANCHOR_TYPE) { // Menu options for a hyperlink. menu.setHeaderTitle(result.getExtra()); - menu.add(0, ID_COPY_LINK, 0, context.getString(R.string.context_menu_copy_link)).setOnMenuItemClickListener(handler); + menu.add(0, ID_COPY_LINK, 0, context.getString(R.string.copy_link_to_clipboard)).setOnMenuItemClickListener(handler); menu.add(0, ID_SHARE_LINK, 0, context.getString(R.string.context_menu_share_link)).setOnMenuItemClickListener(handler); } } diff --git a/app/src/main/java/net/gsantner/opoc/util/Callback.java b/app/src/main/java/net/gsantner/opoc/util/Callback.java new file mode 100644 index 00000000..a08ff682 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/Callback.java @@ -0,0 +1,34 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2018- + * https://gsantner.net/ + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ +package net.gsantner.opoc.util; + +@SuppressWarnings("unused") +public class Callback { + public interface a1 { + void callback(A arg1); + } + + public interface a2 { + void callback(A arg1, B arg2); + } + + public interface a3 { + void callback(A arg1, B arg2, C arg3); + } + + public interface a4 { + void callback(A arg1, B arg2, C arg3, D arg4); + } + + public interface a5 { + void callback(A arg1, B arg2, C arg3, D arg4, E arg5); + } +} diff --git a/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java b/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java index 68c0c4d7..17d64424 100644 --- a/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java @@ -31,6 +31,7 @@ import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.VectorDrawable; +import android.media.MediaScannerConnection; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; @@ -446,6 +447,25 @@ public class ContextUtils { return dp * _context.getResources().getDisplayMetrics().density; } + /** + * Request the givens paths to be scanned by MediaScanner + * + * @param files Files and folders to scan + */ + public void mediaScannerScanFile(File... files) { + if (android.os.Build.VERSION.SDK_INT > 19) { + String[] paths = new String[files.length]; + for (int i = 0; i < files.length; i++) { + paths[i] = files[i].getAbsolutePath(); + } + MediaScannerConnection.scanFile(_context, paths, null, null); + } else { + for (File file : files) { + _context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); + } + } + } + /** * Load an image into a {@link ImageView} and apply a color filter */ @@ -459,7 +479,10 @@ public class ContextUtils { */ public Bitmap drawableToBitmap(Drawable drawable) { Bitmap bitmap = null; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && (drawable instanceof VectorDrawable || drawable instanceof VectorDrawableCompat || drawable instanceof AdaptiveIconDrawable)) { + if (drawable instanceof VectorDrawableCompat + || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && drawable instanceof VectorDrawable) + || ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable instanceof AdaptiveIconDrawable))) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { drawable = (DrawableCompat.wrap(drawable)).mutate(); } diff --git a/app/src/main/java/net/gsantner/opoc/util/FileUtils.java b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java new file mode 100644 index 00000000..13cb66d5 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/FileUtils.java @@ -0,0 +1,340 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2017- + * https://gsantner.net/ + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ +package net.gsantner.opoc.util; + + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.UUID; +import java.util.regex.Pattern; + +@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "SpellCheckingInspection", "deprecation"}) +public class FileUtils { + // Used on methods like copyFile(src, dst) + private static final int BUFFER_SIZE = 4096; + + public static String readTextFile(final File file) { + try { + return readCloseTextStream(new FileInputStream(file)); + } catch (FileNotFoundException e) { + System.err.println("readTextFile: File " + file + " not found."); + } + + return ""; + } + + public static String readCloseTextStream(final InputStream stream) { + return readCloseTextStream(stream, true).get(0); + } + + public static List readCloseTextStream(final InputStream stream, boolean concatToOneString) { + final ArrayList lines = new ArrayList<>(); + BufferedReader reader = null; + String line = ""; + try { + StringBuilder sb = new StringBuilder(); + reader = new BufferedReader(new InputStreamReader(stream)); + + while ((line = reader.readLine()) != null) { + if (concatToOneString) { + sb.append(line).append('\n'); + } else { + lines.add(line); + } + } + line = sb.toString(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + if (concatToOneString) { + lines.clear(); + lines.add(line); + } + return lines; + } + + public static byte[] readBinaryFile(final File file) { + try { + return readCloseBinaryStream(new FileInputStream(file), (int) file.length()); + } catch (FileNotFoundException e) { + System.err.println("readBinaryFile: File " + file + " not found."); + } + + return new byte[0]; + } + + public static byte[] readCloseBinaryStream(final InputStream stream, int byteCount) { + final ArrayList lines = new ArrayList<>(); + BufferedInputStream reader = null; + byte[] buf = new byte[byteCount]; + int totalBytesRead = 0; + try { + reader = new BufferedInputStream(stream); + while (totalBytesRead < byteCount) { + int bytesRead = reader.read(buf, totalBytesRead, byteCount - totalBytesRead); + if (bytesRead > 0) { + totalBytesRead = totalBytesRead + bytesRead; + } + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return buf; + } + + // Read binary stream (of unknown conf size) + public static byte[] readCloseBinaryStream(final InputStream stream) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + byte[] buffer = new byte[BUFFER_SIZE]; + int read; + while ((read = stream.read(buffer)) != -1) { + baos.write(buffer, 0, read); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return baos.toByteArray(); + } + + public static boolean writeFile(final File file, byte[] data) { + try { + OutputStream output = null; + try { + output = new BufferedOutputStream(new FileOutputStream(file)); + output.write(data); + output.flush(); + return true; + } finally { + if (output != null) { + output.close(); + } + } + } catch (Exception ex) { + return false; + } + } + + public static boolean writeFile(final File file, final String content) { + BufferedWriter writer = null; + try { + if (!file.getParentFile().isDirectory() && !file.getParentFile().mkdirs()) + return false; + + writer = new BufferedWriter(new FileWriter(file)); + writer.write(content); + writer.flush(); + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + public static boolean copyFile(final File src, final File dst) { + // Just touch file if src is empty + if (src.length() == 0) { + return touch(dst); + } + + InputStream is = null; + FileOutputStream os = null; + try { + try { + is = new FileInputStream(src); + os = new FileOutputStream(dst); + byte[] buf = new byte[BUFFER_SIZE]; + int len; + while ((len = is.read(buf)) > 0) { + os.write(buf, 0, len); + } + return true; + } finally { + if (is != null) { + is.close(); + } + if (os != null) { + os.close(); + } + } + } catch (IOException ex) { + return false; + } + } + + // Returns -1 if the file did not contain any of the needles, otherwise, + // the index of which needle was found in the contents of the file. + // + // Needless MUST be in lower-case. + public static int fileContains(File file, String... needles) { + try { + FileInputStream in = new FileInputStream(file); + + int i; + String line; + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + while ((line = reader.readLine()) != null) { + for (i = 0; i != needles.length; ++i) + if (line.toLowerCase(Locale.ROOT).contains(needles[i])) { + return i; + } + } + + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + return -1; + } + + public static boolean deleteRecursive(final File file) { + boolean ok = true; + if (file.exists()) { + if (file.isDirectory()) { + for (File child : file.listFiles()) + ok &= deleteRecursive(child); + } + ok &= file.delete(); + } + return ok; + } + + // Example: Check if this is maybe a conf: (str, "jpg", "png", "jpeg") + public static boolean hasExtension(String str, String... extensions) { + String lc = str.toLowerCase(Locale.ROOT); + for (String extension : extensions) { + if (lc.endsWith("." + extension.toLowerCase(Locale.ROOT))) { + return true; + } + } + return false; + } + + public static boolean renameFile(File srcFile, File destFile) { + if (srcFile.getAbsolutePath().equals(destFile.getAbsolutePath())) { + return false; + } + + // renameTo will fail in case of case-changed filename in same dir.Even on case-sensitive FS!!! + if (srcFile.getParent().equals(destFile.getParent()) && srcFile.getName().toLowerCase(Locale.getDefault()).equals(destFile.getName().toLowerCase(Locale.getDefault()))) { + File tmpFile = new File(destFile.getParent(), UUID.randomUUID().getLeastSignificantBits() + ".tmp"); + if (!tmpFile.exists()) { + renameFile(srcFile, tmpFile); + srcFile = tmpFile; + } + } + + if (!srcFile.renameTo(destFile)) { + if (copyFile(srcFile, destFile) && !srcFile.delete()) { + if (!destFile.delete()) { + return false; + } + return false; + } + } + return true; + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + public static boolean renameFileInSameFolder(File srcFile, String destFilename) { + return renameFile(srcFile, new File(srcFile.getParent(), destFilename)); + } + + public static boolean touch(File file) { + try { + if (!file.exists()) { + new FileOutputStream(file).close(); + } + return file.setLastModified(System.currentTimeMillis()); + } catch (IOException e) { + return false; + } + } + + // Get relative path to specified destination + public static String relativePath(File src, File dest) { + try { + String[] srcSplit = (src.isDirectory() ? src : src.getParentFile()).getCanonicalPath().split(Pattern.quote(File.separator)); + String[] destSplit = dest.getCanonicalPath().split(Pattern.quote(File.separator)); + StringBuilder sb = new StringBuilder(); + int i = 0; + + for (; i < destSplit.length && i < srcSplit.length; ++i) { + if (!destSplit[i].equals(srcSplit[i])) + break; + } + if (i != srcSplit.length) { + for (int iUpperDir = i; iUpperDir < srcSplit.length; ++iUpperDir) { + sb.append(".."); + sb.append(File.separator); + } + } + for (; i < destSplit.length; ++i) { + sb.append(destSplit[i]); + sb.append(File.separator); + } + if (!dest.getPath().endsWith("/") && !dest.getPath().endsWith("\\")) { + sb.delete(sb.length() - File.separator.length(), sb.length()); + } + return sb.toString(); + } catch (IOException | NullPointerException exception) { + return null; + } + } +} diff --git a/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java b/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java new file mode 100644 index 00000000..f4586d7b --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/NetworkUtils.java @@ -0,0 +1,208 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2017- + * https://gsantner.net/ + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ +package net.gsantner.opoc.util; + +import org.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "SpellCheckingInspection", "deprecation"}) +public class NetworkUtils { + private static final String UTF8 = "UTF-8"; + public static final String GET = "GET"; + public static final String POST = "POST"; + public static final String PATCH = "PATCH"; + + private final static int BUFFER_SIZE = 4096; + + // Downloads a file from the give url to the output file + // Creates the file's parent directory if it doesn't exist + public static boolean downloadFile(final String url, final File out) { + return downloadFile(url, out, null); + } + + public static boolean downloadFile(final String url, final File out, final Callback.a1 progressCallback) { + try { + return downloadFile(new URL(url), out, progressCallback); + } catch (MalformedURLException e) { + // Won't happen + e.printStackTrace(); + return false; + } + } + + public static boolean downloadFile(final URL url, final File outFile, final Callback.a1 progressCallback) { + InputStream input = null; + OutputStream output = null; + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) url.openConnection(); + connection.connect(); + input = connection.getInputStream(); + + if (!outFile.getParentFile().isDirectory()) + if (!outFile.getParentFile().mkdirs()) + return false; + output = new FileOutputStream(outFile); + + int count; + int written = 0; + final float invLength = 1f / connection.getContentLength(); + + byte data[] = new byte[BUFFER_SIZE]; + while ((count = input.read(data)) != -1) { + output.write(data, 0, count); + if (invLength != -1f && progressCallback != null) { + written += count; + progressCallback.callback(written * invLength); + } + } + + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } finally { + try { + if (output != null) + output.close(); + if (input != null) + input.close(); + } catch (IOException ignored) { + } + if (connection != null) + connection.disconnect(); + } + } + + // No parameters, method can be GET, POST, etc. + public static String performCall(final String url, final String method) { + try { + return performCall(new URL(url), method, ""); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + return ""; + } + + public static String performCall(final String url, final String method, final String data) { + try { + return performCall(new URL(url), method, data); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + return ""; + } + + // URL encoded parameters + public static String performCall(final String url, final String method, final HashMap params) { + try { + return performCall(new URL(url), method, encodeQuery(params)); + } catch (UnsupportedEncodingException | MalformedURLException e) { + e.printStackTrace(); + } + return ""; + } + + // Defaults to POST + public static String performCall(final String url, final JSONObject json) { + return performCall(url, POST, json); + } + + public static String performCall(final String url, final String method, final JSONObject json) { + try { + return performCall(new URL(url), method, json.toString()); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + return ""; + } + + private static String performCall(final URL url, final String method, final String data) { + try { + final HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod(method); + conn.setDoInput(true); + + if (data != null && !data.isEmpty()) { + conn.setDoOutput(true); + final OutputStream output = conn.getOutputStream(); + output.write(data.getBytes(Charset.forName(UTF8))); + output.flush(); + output.close(); + } + + return FileUtils.readCloseTextStream(conn.getInputStream()); + } catch (Exception e) { + e.printStackTrace(); + } + return ""; + } + + private static String encodeQuery(final HashMap params) throws UnsupportedEncodingException { + final StringBuilder result = new StringBuilder(); + boolean first = true; + for (Map.Entry entry : params.entrySet()) { + if (first) first = false; + else result.append("&"); + + result.append(URLEncoder.encode(entry.getKey(), UTF8)); + result.append("="); + result.append(URLEncoder.encode(entry.getValue(), UTF8)); + } + + return result.toString(); + } + + public static HashMap getDataMap(final String query) { + final HashMap result = new HashMap<>(); + final StringBuilder sb = new StringBuilder(); + String name = ""; + + try { + for (int i = 0; i < query.length(); i++) { + char c = query.charAt(i); + switch (c) { + case '=': + name = URLDecoder.decode(sb.toString(), UTF8); + sb.setLength(0); + break; + case '&': + result.put(name, URLDecoder.decode(sb.toString(), UTF8)); + sb.setLength(0); + break; + default: + sb.append(c); + break; + } + } + if (!name.isEmpty()) + result.put(name, URLDecoder.decode(sb.toString(), UTF8)); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + return result; + } +} diff --git a/app/src/main/java/net/gsantner/opoc/util/PermissionChecker.java b/app/src/main/java/net/gsantner/opoc/util/PermissionChecker.java new file mode 100644 index 00000000..524c3065 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/PermissionChecker.java @@ -0,0 +1,70 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2017- + * https://gsantner.net/ + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ +package net.gsantner.opoc.util; + +import android.Manifest; +import android.app.Activity; +import android.content.pm.PackageManager; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AlertDialog; + +import java.io.File; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public class PermissionChecker { + private static final int CODE_PERMISSION_EXTERNAL_STORAGE = 4000; + + private Activity _activity; + + public PermissionChecker(Activity activity) { + _activity = activity; + } + + public boolean doIfExtStoragePermissionGranted(String... optionalToastMessageForKnowingWhyNeeded) { + if (ContextCompat.checkSelfPermission(_activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + + if (optionalToastMessageForKnowingWhyNeeded != null && optionalToastMessageForKnowingWhyNeeded.length > 0 && optionalToastMessageForKnowingWhyNeeded[0] != null) { + new AlertDialog.Builder(_activity) + .setMessage(optionalToastMessageForKnowingWhyNeeded[0]) + .setCancelable(false) + .setNegativeButton(android.R.string.no, null) + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + if (android.os.Build.VERSION.SDK_INT >= 23) { + ActivityCompat.requestPermissions(_activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, CODE_PERMISSION_EXTERNAL_STORAGE); + } + }) + .show(); + return false; + } + ActivityCompat.requestPermissions(_activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, CODE_PERMISSION_EXTERNAL_STORAGE); + return false; + } + return true; + } + + public boolean checkPermissionResult(int requestCode, String[] permissions, int[] grantResults) { + if (grantResults.length > 0) { + switch (requestCode) { + case CODE_PERMISSION_EXTERNAL_STORAGE: { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + return true; + } + } + } + } + return false; + } + + public boolean mkdirIfStoragePermissionGranted(File dir) { + return doIfExtStoragePermissionGranted() && (dir.exists() || dir.mkdirs()); + } +} diff --git a/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java b/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java new file mode 100644 index 00000000..21932cd6 --- /dev/null +++ b/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java @@ -0,0 +1,453 @@ +/*####################################################### + * + * Maintained by Gregor Santner, 2017- + * https://gsantner.net/ + * + * License: Apache 2.0 + * https://github.com/gsantner/opoc/#licensing + * https://www.apache.org/licenses/LICENSE-2.0 + * +#########################################################*/ +package net.gsantner.opoc.util; + +import android.app.Activity; +import android.content.ClipData; +import android.content.Context; +import android.content.Intent; +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.print.PrintAttributes; +import android.print.PrintDocumentAdapter; +import android.print.PrintJob; +import android.print.PrintManager; +import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.support.v4.content.FileProvider; +import android.support.v4.content.pm.ShortcutInfoCompat; +import android.support.v4.content.pm.ShortcutManagerCompat; +import android.support.v4.graphics.drawable.IconCompat; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.webkit.WebView; + +import java.io.File; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +/** + * A utility class to ease information sharing on Android + * Also allows to parse/fetch information out of shared information + */ +@SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "SameParameterValue", "unused", "deprecation", "ConstantConditions", "ObsoleteSdkInt", "SpellCheckingInspection"}) +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", Locale.getDefault()); + public final static SimpleDateFormat SDF_SHORT = new SimpleDateFormat("yyMMdd-HHmm", Locale.getDefault()); + + + protected Context _context; + protected String _fileProviderAuthority; + protected String _chooserTitle; + + public ShareUtil(Context context) { + _context = context; + _chooserTitle = "➥"; + } + + public String getFileProviderAuthority() { + if (TextUtils.isEmpty(_fileProviderAuthority)) { + throw new RuntimeException("Error at ShareUtil.getFileProviderAuthority(): No FileProvider authority provided"); + } + return _fileProviderAuthority; + } + + public ShareUtil setFileProviderAuthority(String fileProviderAuthority) { + _fileProviderAuthority = fileProviderAuthority; + return this; + } + + + public ShareUtil setChooserTitle(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(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(Intent intent, String chooserText) { + _context.startActivity(Intent.createChooser(intent, + chooserText != null ? chooserText : _chooserTitle)); + } + + /** + * 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(Intent intent, @DrawableRes int iconRes, 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(Intent intent, @DrawableRes int iconRes, 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(String text, @Nullable String mimeType) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_TEXT, text); + intent.setType(mimeType != null ? mimeType : "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 void shareStream(File file, String mimeType) { + Uri fileUri = FileProvider.getUriForFile(_context, getFileProviderAuthority(), file); + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_STREAM, fileUri); + intent.putExtra(EXTRA_FILEPATH, file.getAbsolutePath()); + intent.setType(mimeType); + showChooser(intent, null); + } + + /** + * 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(Bitmap bitmap, 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(Bitmap bitmap, Bitmap.CompressFormat format, int quality, 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) + @SuppressWarnings("deprecation") + public PrintJob print(WebView webview, String jobName) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + PrintDocumentAdapter printAdapter; + PrintManager printManager = (PrintManager) webview.getContext().getSystemService(Context.PRINT_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + printAdapter = webview.createPrintDocumentAdapter(jobName); + } else { + printAdapter = webview.createPrintDocumentAdapter(); + } + if (printManager != null) { + return printManager.print(jobName, printAdapter, new PrintAttributes.Builder().build()); + } + } 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) + @SuppressWarnings("deprecation") + public PrintJob createPdf(WebView webview, 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(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(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); + cm.setPrimaryClip(clip); + 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, 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(String subject, String body, 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(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))) { + // 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"}) { + if (fileStr.startsWith(prefix)) { + fileStr = fileStr.substring(prefix.length()); + } + } + // 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()))); + } + // 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; + } + } + } + } + } + return null; + } +} diff --git a/app/src/main/res/menu/stream__menu_top.xml b/app/src/main/res/menu/stream__menu_top.xml index 34294396..36a410b8 100644 --- a/app/src/main/res/menu/stream__menu_top.xml +++ b/app/src/main/res/menu/stream__menu_top.xml @@ -14,9 +14,19 @@ + + + diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 9ead7ce6..80d95656 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -30,7 +30,7 @@ Del billede Åben i ekstern browser… - Kopier link-adresse til udklipsholder + Kopier link-adresse til udklipsholder diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index bbf94f0a..239ae35f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -57,7 +57,7 @@ Bild speichern Bild teilen In externem Browser öffnen… - Link-Adresse kopieren + Link-Adresse kopieren Bild-Adresse kopieren Konnte Bild nicht laden… diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 6969ce9f..2d83a51b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -57,7 +57,7 @@ Guardar imagen Compartir imagen Abrir en navegador externo… - Copiar dirección del enlace al portapapeles + Copiar dirección del enlace al portapapeles Copiar dirección de imagen al portapapeles No se pudo cargar la imagen diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ceba8114..bb0866d0 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -57,7 +57,7 @@ Enregistrer l\'image Partager l\'image Ouvrir dans un navigateur externe… - Copier le lien dans le presse-papier + Copier le lien dans le presse-papier Copier le lien de l\'image dans le presse-papiers Impossible de récupérer l\'image diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 50fec360..ed7041cb 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -57,7 +57,7 @@ Gardar imaxe Compartir imaxe Abrir nun navegador externo… - Copiar ligazón ao portapapeis + Copiar ligazón ao portapapeis Copia enderezo da imaxe ao portapapeis Non se cargou a imaxe diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 378ea528..3d04a9c0 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -57,7 +57,7 @@ Kép mentése Kép megosztása Megnyitás külső böngészőben… - Link címének másolása a vágólapra + Link címének másolása a vágólapra Kép címének másolása a vágólapra Nem lehet betölteni a képet diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 00117c36..d5b5885f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -57,7 +57,7 @@ Salva immagine Condividi immagine Apri nel browser… - Copia link negli appunti + Copia link negli appunti Copia indirizzo dell\'immagine negli appunti Impossibile caricare immagine diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 0ee47968..6b7ace50 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -57,7 +57,7 @@ 画像を保存 画像をシェア 外部ブラウザーで開く… - リンクアドレスをクリップボードへコピー + リンクアドレスをクリップボードへコピー 画像のアドレスをクリップボードへコピー 画像を読み込むことができません diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 92c2885b..4e6fe374 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -57,7 +57,7 @@ Sekles tugna Bḍu tugna Ldi deg iminig azɣaray… - Nɣel aseɣwen ɣef afus + Nɣel aseɣwen ɣef afus Nɣel tugna ɣef afus Ur izmir ara ad d-isali tugna diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 4aebdff8..bb300a60 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -57,7 +57,7 @@ ചിത്രം സംരക്ഷിക്കുക ചിത്രം പങ്കുവയ്ക്കുക പുറമെയുള്ള ബ്രൗസറിൽ തുറക്കുക… - ലിങ്ക് വിലാസം ക്ലിപ്ബോർഡിലേക്ക് പകർത്തുക + ലിങ്ക് വിലാസം ക്ലിപ്ബോർഡിലേക്ക് പകർത്തുക ചിത്രത്തിന്റെ വിലാസം ക്ലിപ്ബോർഡിലേക്ക് പകർത്തുക ചിത്രം ലോഡ് ചെയ്യാൻ സാധിക്കുന്നില്ല diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 22a36fb8..481ce9f1 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -57,7 +57,7 @@ Afbeelding opslaan Deel afbeelding Geopend in externe browser… - Link-adres kopiëren naar Klembord + Link-adres kopiëren naar Klembord Afbeelding kopiëren naar Klembord Niet in staat om afbeelding te laden diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 7b5cdc34..2c3e8e68 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -57,7 +57,7 @@ Zapisz obraz Udostępnij obraz Otwórz w zewnętrznej przeglądarce… - Skopiuj adres odnośnika do schowka + Skopiuj adres odnośnika do schowka Skopiuj adres obrazu do schowka Nie udało się wczytać obrazu diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6a54eeec..1720bd5f 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -59,7 +59,7 @@ Salvar imagem Compartilhar imagem Abrir em navegador externo… - Copiar endereço à área de transferência + Copiar endereço à área de transferência Copiar endereço de imagem à área de transferência Impossível carregar a imagem diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a0271522..0073f07c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -57,7 +57,7 @@ Сохранить изображение Поделиться изображением Открыть во внешнем браузере… - Скопировать адрес ссылки в буфер обмена + Скопировать адрес ссылки в буфер обмена Скопировать адрес изображения в буфер обмена Не удаётся загрузить изображение diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml index 72b3bbd8..b7ca8fc1 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -71,7 +71,7 @@ Sarva s\'immàgine Cumpartzi s\'immàgine Aberi in un\'esploradore (browser) esternu… - Còpia su ligàmenes in sos apuntos + Còpia su ligàmenes in sos apuntos Còpia s\'indiritzu de s\'immàgine in sos apuntos diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 5664fd09..5eb05d34 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -57,7 +57,7 @@ Spara bild Dela bild Öppna i en extern webbläsare… - Kopiera länkadress + Kopiera länkadress Kopiera bildadressen Kunde inte ladda bilden diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml index b7a2d304..25c3ad87 100644 --- a/app/src/main/res/values-uk-rUA/strings.xml +++ b/app/src/main/res/values-uk-rUA/strings.xml @@ -57,7 +57,7 @@ Зберегти зображення Поділитися зображенням Відкрити у зовнішньому браузері… - Копіювати адресу посилання у буфер обміну + Копіювати адресу посилання у буфер обміну Копіювати зображення у буфер обміну Не вдалося завантажити зображення diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 79671b48..e5b84697 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -57,7 +57,7 @@ 儲存圖片 分享圖片 用外部瀏覽器開啟… - 將連結網址複製到剪貼簿 + 將連結網址複製到剪貼簿 將圖片網址複製到剪貼簿 無法載入圖片 diff --git a/app/src/main/res/values/strings-not_translatable.xml b/app/src/main/res/values/strings-not_translatable.xml index 4ec47306..4e210a01 100644 --- a/app/src/main/res/values/strings-not_translatable.xml +++ b/app/src/main/res/values/strings-not_translatable.xml @@ -127,4 +127,5 @@ pref_key__is_overview_statusbar_hidden pref_key__recreate_main_activity pref_key__show_title + PDF diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index db79e82c..7a40626b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -70,7 +70,7 @@ Save image Share image Open in external browser… - Copy link address to clipboard + Copy link address to clipboard Copy image address to clipboard @@ -98,4 +98,5 @@ Hide statusbar Show title in the main view Show title + Launcher shortcut diff --git a/metadata/en-US/full_description.txt b/metadata/en-US/full_description.txt index 120c4be9..651f7602 100644 --- a/metadata/en-US/full_description.txt +++ b/metadata/en-US/full_description.txt @@ -3,16 +3,16 @@ It adds useful features to your networking experience: ⚡ Quick access to most diaspora* features 👉 Share content to and from the app -🌎 Proxy support +🌎 Proxy support (Tor/Orbot supported) 📰 In-app-browser to view articles 🎨 Customizeable colors 🌆 Night/AMOLED mode 🈴 Localized in many languages ✔️ Allows system independent language - Support the project:Translate using StringlateJoin discussion on MatrixMore information about contributions + ✋ Android Contribution Guide (gsantner blog)Support main developer