Rework screenshot saving and sharing; add new share options:

* Share option: Launcher shortcut (fixes #170)
* Share option: Copy link of current page to clipboard
* Share otpion: Export as PDF / print
This commit is contained in:
Gregor Santner 2018-04-08 17:52:04 +02:00
parent d53128e5cb
commit 51093e0c3d
No known key found for this signature in database
GPG Key ID: 7E83A7834AECB009
35 changed files with 1222 additions and 138 deletions

View File

@ -7,6 +7,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<application
android:name="com.github.dfa.diaspora_android.App"

View File

@ -25,6 +25,7 @@ import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@ -39,8 +40,10 @@ import android.webkit.JavascriptInterface;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.widget.Toast;
import com.github.dfa.diaspora_android.App;
import com.github.dfa.diaspora_android.BuildConfig;
import com.github.dfa.diaspora_android.R;
import com.github.dfa.diaspora_android.data.DiasporaUserProfile;
import com.github.dfa.diaspora_android.ui.theme.ThemedAlertDialogBuilder;
@ -53,10 +56,14 @@ import com.github.dfa.diaspora_android.web.DiasporaStreamWebChromeClient;
import com.github.dfa.diaspora_android.web.FileUploadWebChromeClient;
import com.github.dfa.diaspora_android.web.WebHelper;
import net.gsantner.opoc.util.PermissionChecker;
import net.gsantner.opoc.util.ShareUtil;
import org.json.JSONException;
import java.io.File;
import java.io.IOException;
import java.util.Date;
/**
* Fragment that displays the Stream of the diaspora* user
@ -97,6 +104,9 @@ public class DiasporaStreamFragment extends BrowserFragment {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.stream__menu_top, menu);
if (Build.VERSION.SDK_INT >= 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;
}
}

View File

@ -315,8 +315,8 @@ public class DiasporaPodList implements Iterable<DiasporaPodList.DiasporaPod>, S
}
/*
* Getter & Setter
*/
* Getter & Setter
*/
public List<DiasporaPodUrl> getPodUrls() {
return _podUrls;
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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<A> {
void callback(A arg1);
}
public interface a2<A, B> {
void callback(A arg1, B arg2);
}
public interface a3<A, B, C> {
void callback(A arg1, B arg2, C arg3);
}
public interface a4<A, B, C, D> {
void callback(A arg1, B arg2, C arg3, D arg4);
}
public interface a5<A, B, C, D, E> {
void callback(A arg1, B arg2, C arg3, D arg4, E arg5);
}
}

View File

@ -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();
}

View File

@ -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<String> readCloseTextStream(final InputStream stream, boolean concatToOneString) {
final ArrayList<String> 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<String> 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;
}
}
}

View File

@ -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<Float> 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<Float> 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<String, String> 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<String, String> params) throws UnsupportedEncodingException {
final StringBuilder result = new StringBuilder();
boolean first = true;
for (Map.Entry<String, String> 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<String, String> getDataMap(final String query) {
final HashMap<String, String> 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;
}
}

View File

@ -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());
}
}

View File

@ -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:
* <uses-permission android:name="android.permission.INSTALL_SHORTCUT" />
* <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
*
* @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:
* <uses-permission android:name="android.permission.INSTALL_SHORTCUT" />
* <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
*
* @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<String> getClipboard() {
List<String> 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<Boolean, String> 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;
}
}

View File

@ -14,9 +14,19 @@
<item
android:id="@+id/action_take_screenshot"
android:title="@string/share__take_screenshot" />
<item
android:id="@+id/action_share_pdf"
android:title="@string/pdf"
android:visible="false" />
<item
android:id="@+id/action_share_link"
android:title="@string/share__share_link_as_text" />
<item
android:id="@+id/action_share_link_to_clipboard"
android:title="@string/copy_link_to_clipboard" />
<item
android:id="@+id/action_create_launcher_shortcut"
android:title="@string/launcher_shortcut" />
</menu>
</item>

View File

@ -30,7 +30,7 @@
<!-- Drawer, Menu, Toolbar, ContextMenu -->
<string name="context_menu_share_image">Del billede</string>
<string name="context_menu_open_external_browser">Åben i ekstern browser&#8230;</string>
<string name="context_menu_copy_link">Kopier link-adresse til udklipsholder</string>
<string name="copy_link_to_clipboard">Kopier link-adresse til udklipsholder</string>
<!-- More from MainActivity -->
<!-- Permissions -->
</resources>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">Bild speichern</string>
<string name="context_menu_share_image">Bild teilen</string>
<string name="context_menu_open_external_browser">In externem Browser öffnen&#8230;</string>
<string name="context_menu_copy_link">Link-Adresse kopieren</string>
<string name="copy_link_to_clipboard">Link-Adresse kopieren</string>
<string name="context_menu_copy_image_link">Bild-Adresse kopieren</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">Konnte Bild nicht laden…</string>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">Guardar imagen</string>
<string name="context_menu_share_image">Compartir imagen</string>
<string name="context_menu_open_external_browser">Abrir en navegador externo&#8230;</string>
<string name="context_menu_copy_link">Copiar dirección del enlace al portapapeles</string>
<string name="copy_link_to_clipboard">Copiar dirección del enlace al portapapeles</string>
<string name="context_menu_copy_image_link">Copiar dirección de imagen al portapapeles</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">No se pudo cargar la imagen</string>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">Enregistrer l\'image</string>
<string name="context_menu_share_image">Partager l\'image</string>
<string name="context_menu_open_external_browser">Ouvrir dans un navigateur externe&#8230;</string>
<string name="context_menu_copy_link">Copier le lien dans le presse-papier</string>
<string name="copy_link_to_clipboard">Copier le lien dans le presse-papier</string>
<string name="context_menu_copy_image_link">Copier le lien de l\'image dans le presse-papiers</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">Impossible de récupérer l\'image</string>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">Gardar imaxe</string>
<string name="context_menu_share_image">Compartir imaxe</string>
<string name="context_menu_open_external_browser">Abrir nun navegador externo&#8230;</string>
<string name="context_menu_copy_link">Copiar ligazón ao portapapeis</string>
<string name="copy_link_to_clipboard">Copiar ligazón ao portapapeis</string>
<string name="context_menu_copy_image_link">Copia enderezo da imaxe ao portapapeis</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">Non se cargou a imaxe</string>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">Kép mentése</string>
<string name="context_menu_share_image">Kép megosztása</string>
<string name="context_menu_open_external_browser">Megnyitás külső böngészőben&#8230;</string>
<string name="context_menu_copy_link">Link címének másolása a vágólapra</string>
<string name="copy_link_to_clipboard">Link címének másolása a vágólapra</string>
<string name="context_menu_copy_image_link">Kép címének másolása a vágólapra</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">Nem lehet betölteni a képet</string>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">Salva immagine</string>
<string name="context_menu_share_image">Condividi immagine</string>
<string name="context_menu_open_external_browser">Apri nel browser&#8230;</string>
<string name="context_menu_copy_link">Copia link negli appunti</string>
<string name="copy_link_to_clipboard">Copia link negli appunti</string>
<string name="context_menu_copy_image_link">Copia indirizzo dell\'immagine negli appunti</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">Impossibile caricare immagine</string>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">画像を保存</string>
<string name="context_menu_share_image">画像をシェア</string>
<string name="context_menu_open_external_browser">外部ブラウザーで開く&#8230;</string>
<string name="context_menu_copy_link">リンクアドレスをクリップボードへコピー</string>
<string name="copy_link_to_clipboard">リンクアドレスをクリップボードへコピー</string>
<string name="context_menu_copy_image_link">画像のアドレスをクリップボードへコピー</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">画像を読み込むことができません</string>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">Sekles tugna</string>
<string name="context_menu_share_image">Bḍu tugna</string>
<string name="context_menu_open_external_browser">Ldi deg iminig azɣaray&#8230;</string>
<string name="context_menu_copy_link">Nɣel aseɣwen ɣef afus</string>
<string name="copy_link_to_clipboard">Nɣel aseɣwen ɣef afus</string>
<string name="context_menu_copy_image_link">Nɣel tugna ɣef afus</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">Ur izmir ara ad d-isali tugna</string>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">ചിത്രം സംരക്ഷിക്കുക</string>
<string name="context_menu_share_image">ചിത്രം പങ്കുവയ്ക്കുക</string>
<string name="context_menu_open_external_browser">പുറമെയുള്ള ബ്രൗസറിൽ തുറക്കുക&#8230;</string>
<string name="context_menu_copy_link">ലിങ്ക് വിലാസം ക്ലിപ്ബോർഡിലേക്ക് പകർത്തുക</string>
<string name="copy_link_to_clipboard">ലിങ്ക് വിലാസം ക്ലിപ്ബോർഡിലേക്ക് പകർത്തുക</string>
<string name="context_menu_copy_image_link">ചിത്രത്തിന്റെ വിലാസം ക്ലിപ്ബോർഡിലേക്ക് പകർത്തുക</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">ചിത്രം ലോഡ് ചെയ്യാൻ സാധിക്കുന്നില്ല</string>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">Afbeelding opslaan</string>
<string name="context_menu_share_image">Deel afbeelding</string>
<string name="context_menu_open_external_browser">Geopend in externe browser&#8230;</string>
<string name="context_menu_copy_link">Link-adres kopiëren naar Klembord</string>
<string name="copy_link_to_clipboard">Link-adres kopiëren naar Klembord</string>
<string name="context_menu_copy_image_link">Afbeelding kopiëren naar Klembord</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">Niet in staat om afbeelding te laden</string>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">Zapisz obraz</string>
<string name="context_menu_share_image">Udostępnij obraz</string>
<string name="context_menu_open_external_browser">Otwórz w zewnętrznej przeglądarce&#8230;</string>
<string name="context_menu_copy_link">Skopiuj adres odnośnika do schowka</string>
<string name="copy_link_to_clipboard">Skopiuj adres odnośnika do schowka</string>
<string name="context_menu_copy_image_link">Skopiuj adres obrazu do schowka</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">Nie udało się wczytać obrazu</string>

View File

@ -59,7 +59,7 @@
<string name="context_menu_save_image">Salvar imagem</string>
<string name="context_menu_share_image">Compartilhar imagem</string>
<string name="context_menu_open_external_browser">Abrir em navegador externo&#8230;</string>
<string name="context_menu_copy_link">Copiar endereço à área de transferência</string>
<string name="copy_link_to_clipboard">Copiar endereço à área de transferência</string>
<string name="context_menu_copy_image_link">Copiar endereço de imagem à área de transferência</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">Impossível carregar a imagem</string>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">Сохранить изображение</string>
<string name="context_menu_share_image">Поделиться изображением</string>
<string name="context_menu_open_external_browser">Открыть во внешнем браузере&#8230;</string>
<string name="context_menu_copy_link">Скопировать адрес ссылки в буфер обмена</string>
<string name="copy_link_to_clipboard">Скопировать адрес ссылки в буфер обмена</string>
<string name="context_menu_copy_image_link">Скопировать адрес изображения в буфер обмена</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">Не удаётся загрузить изображение</string>

View File

@ -71,7 +71,7 @@
<string name="context_menu_save_image">Sarva s\'immàgine</string>
<string name="context_menu_share_image">Cumpartzi s\'immàgine</string>
<string name="context_menu_open_external_browser">Aberi in un\'esploradore (browser) esternu…</string>
<string name="context_menu_copy_link">Còpia su ligàmenes in sos apuntos</string>
<string name="copy_link_to_clipboard">Còpia su ligàmenes in sos apuntos</string>
<string name="context_menu_copy_image_link">Còpia s\'indiritzu de s\'immàgine in sos apuntos</string>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">Spara bild</string>
<string name="context_menu_share_image">Dela bild</string>
<string name="context_menu_open_external_browser">Öppna i en extern webbläsare&#8230;</string>
<string name="context_menu_copy_link">Kopiera länkadress</string>
<string name="copy_link_to_clipboard">Kopiera länkadress</string>
<string name="context_menu_copy_image_link">Kopiera bildadressen</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">Kunde inte ladda bilden</string>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">Зберегти зображення</string>
<string name="context_menu_share_image">Поділитися зображенням</string>
<string name="context_menu_open_external_browser">Відкрити у зовнішньому браузері&#8230;</string>
<string name="context_menu_copy_link">Копіювати адресу посилання у буфер обміну</string>
<string name="copy_link_to_clipboard">Копіювати адресу посилання у буфер обміну</string>
<string name="context_menu_copy_image_link">Копіювати зображення у буфер обміну</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">Не вдалося завантажити зображення</string>

View File

@ -57,7 +57,7 @@
<string name="context_menu_save_image">儲存圖片</string>
<string name="context_menu_share_image">分享圖片</string>
<string name="context_menu_open_external_browser">用外部瀏覽器開啟&#8230;</string>
<string name="context_menu_copy_link">將連結網址複製到剪貼簿</string>
<string name="copy_link_to_clipboard">將連結網址複製到剪貼簿</string>
<string name="context_menu_copy_image_link">將圖片網址複製到剪貼簿</string>
<!-- More from MainActivity -->
<string name="unable_to_load_image">無法載入圖片</string>

View File

@ -127,4 +127,5 @@
<string name="pref_key__is_overview_statusbar_hidden" translatable="false">pref_key__is_overview_statusbar_hidden</string>
<string name="pref_key__recreate_main_activity" translatable="false">pref_key__recreate_main_activity</string>
<string name="pref_key__show_title" translatable="false">pref_key__show_title</string>
<string name="pdf" translatable="false">PDF</string>
</resources>

View File

@ -70,7 +70,7 @@
<string name="context_menu_save_image">Save image</string>
<string name="context_menu_share_image">Share image</string>
<string name="context_menu_open_external_browser">Open in external browser…</string>
<string name="context_menu_copy_link">Copy link address to clipboard</string>
<string name="copy_link_to_clipboard">Copy link address to clipboard</string>
<string name="context_menu_copy_image_link">Copy image address to clipboard</string>
@ -98,4 +98,5 @@
<string name="pref_title__is_statusbar_hidden">Hide statusbar</string>
<string name="pref_summary__show_title">Show title in the main view</string>
<string name="pref_title__show_title">Show title</string>
<string name="launcher_shortcut">Launcher shortcut</string>
</resources>

View File

@ -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
<b>Support the project:</b>
✋ <a href="https://lonamiwebs.github.io/stringlate/translate?git=https%3A%2F%2Fgithub.com%2Fdiaspora-for-android%2Fdandelion.git&mail=gro.xobliam@@rentnasg">Translate using Stringlate</a>
✋ <a href="https://matrix.to/#/#dandelion:matrix.org">Join discussion on Matrix</a>
✋ <a href="https://github.com/diaspora-for-android/dandelion#contributions">More information about contributions</a>
✋ <a href="https://gsantner.net/android-contribution-guide/?packageid=com.github.dfa.diaspora_android&name=dandelion&web=https://github.com/diaspora-for-android/dandelion">Android Contribution Guide (gsantner blog)</a>
✋ <a href="http://gsantner.net/supportme?ref=dandelion&source=fdroid">Support main developer</a>