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 Stringlate
✋ Join discussion on Matrix
✋ More information about contributions
+ ✋ Android Contribution Guide (gsantner blog)
✋ Support main developer