Update opoc

This commit is contained in:
Gregor Santner 2019-07-26 03:19:28 +02:00
parent 3ec8ab89c6
commit a618da97d8
No known key found for this signature in database
GPG Key ID: 7E83A7834AECB009
12 changed files with 701 additions and 39 deletions

View File

@ -108,11 +108,13 @@ dependencies {
implementation "com.android.support:support-v4:${version_library_appcompat}"
implementation "com.android.support:customtabs:${version_library_appcompat}"
implementation "com.android.support:cardview-v7:${version_library_appcompat}"
implementation "com.android.support:preference-v7:${version_library_appcompat}"
// UI libraries
implementation "com.github.DASAR:ShiftColorPicker:v0.5"
// Tool libraries
implementation 'commons-io:commons-io:2.6'
implementation "info.guardianproject.netcipher:netcipher:${version_library_netcipher}"
implementation "info.guardianproject.netcipher:netcipher-webkit:${version_library_netcipher}"
implementation "com.jakewharton:butterknife:${version_library_butterknife}"

View File

@ -16,7 +16,10 @@ import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
@ -33,6 +36,7 @@ public abstract class GsFragmentBase extends Fragment {
protected ContextUtils _cu;
protected Bundle _savedInstanceState = null;
protected Menu _fragmentMenu;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -51,6 +55,9 @@ public abstract class GsFragmentBase extends Fragment {
_cu = new ContextUtils(inflater.getContext());
_cu.setAppLanguage(getAppLanguage());
_savedInstanceState = savedInstanceState;
if (getLayoutResId() == 0) {
Log.e(getClass().getCanonicalName(), "Error: GsFragmentbase.onCreateview: Returned 0 for getLayoutResId");
}
View view = inflater.inflate(getLayoutResId(), container, false);
ButterKnife.bind(this, view);
return view;
@ -126,4 +133,14 @@ public abstract class GsFragmentBase extends Fragment {
}
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
_fragmentMenu = menu;
}
public Menu getFragmentMenu() {
return _fragmentMenu;
}
}

View File

@ -10,8 +10,6 @@
#########################################################*/
package net.gsantner.opoc.preference;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
@SuppressWarnings({"UnusedReturnValue", "SpellCheckingInspection", "unused", "SameParameterValue"})

View File

@ -532,7 +532,7 @@ public class SharedPreferencesPropertyBackend implements PropertyBackend<String,
* A method to determine if current hour is between begin and end.
* This is especially useful for time-based light/dark mode
*/
public boolean isCurrentHourOfDayBetween(int begin, int end) {
public static boolean isCurrentHourOfDayBetween(int begin, int end) {
begin = (begin >= 23 || begin < 0) ? 0 : begin;
end = (end >= 23 || end < 0) ? 0 : end;
int h = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
@ -559,7 +559,7 @@ public class SharedPreferencesPropertyBackend implements PropertyBackend<String,
public boolean afterDaysTrue(String key, int daysSinceLastTime, int firstTime, final SharedPreferences... pref) {
Date d = new Date(System.currentTimeMillis());
if (!contains(key)) {
d = getDateOfDaysAgo(daysSinceLastTime-firstTime);
d = getDateOfDaysAgo(daysSinceLastTime - firstTime);
setLong(key, d.getTime());
return firstTime < 1;
} else {

View File

@ -12,10 +12,12 @@ package net.gsantner.opoc.ui;
import android.app.Activity;
import android.graphics.Typeface;
import android.os.AsyncTask;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.AppCompatEditText;
import android.text.Editable;
@ -24,6 +26,7 @@ import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ArrayAdapter;
import android.widget.Filter;
@ -31,12 +34,20 @@ import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import net.gsantner.opoc.util.ActivityUtils;
import net.gsantner.opoc.util.Callback;
import net.gsantner.opoc.util.ContextUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
@SuppressWarnings("WeakerAccess")
public class SearchOrCustomTextDialog {
@ -115,6 +126,7 @@ public class SearchOrCustomTextDialog {
}
};
final ActivityUtils activityUtils = new ActivityUtils(activity);
final AppCompatEditText searchEditText = new AppCompatEditText(activity);
searchEditText.setSingleLine(true);
searchEditText.setMaxLines(1);
@ -140,6 +152,7 @@ public class SearchOrCustomTextDialog {
final ListView listView = new ListView(activity);
final LinearLayout linearLayout = new LinearLayout(activity);
listView.setAdapter(listAdapter);
listView.setVisibility(dopt.data != null && !dopt.data.isEmpty() ? View.VISIBLE : View.GONE);
linearLayout.setOrientation(LinearLayout.VERTICAL);
if (dopt.isSearchEnabled) {
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
@ -157,7 +170,7 @@ public class SearchOrCustomTextDialog {
dialogBuilder.setView(linearLayout)
.setTitle(dopt.titleText)
.setOnCancelListener(null)
.setNegativeButton(dopt.cancelButtonText, null);
.setNegativeButton(dopt.cancelButtonText, (dialogInterface, i) -> dialogInterface.dismiss());
if (dopt.isSearchEnabled) {
dialogBuilder.setPositiveButton(dopt.okButtonText, (dialogInterface, i) -> {
dialogInterface.dismiss();
@ -186,9 +199,124 @@ public class SearchOrCustomTextDialog {
return false;
});
if (dialog.getWindow() != null) {
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
Window w;
if ((w = dialog.getWindow()) != null && dopt.isSearchEnabled) {
w.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
}
dialog.show();
if ((w = dialog.getWindow()) != null) {
w.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT);
}
if (dopt.isSearchEnabled) {
searchEditText.requestFocus();
}
}
public static SearchFilesTask recursiveFileSearch(Activity activity, File searchDir, String query, Callback.a1<List<String>> callback) {
query = query.replaceAll("(?<![.])[*]", ".*");
SearchFilesTask task = new SearchFilesTask(activity, searchDir, query, callback, query.startsWith("^") || query.contains("*"));
task.execute();
return task;
}
public static class SearchFilesTask extends AsyncTask<Void, File, List<String>> implements IOFileFilter {
private final Callback.a1<List<String>> _callback;
private final File _searchDir;
private final String _query;
private final boolean _isRegex;
private final WeakReference<Activity> _activityRef;
private final Pattern _regex;
private Snackbar _snackBar;
public SearchFilesTask(Activity activity, File searchDir, String query, Callback.a1<List<String>> callback, boolean isRegex) {
_searchDir = searchDir;
_query = isRegex ? query : query.toLowerCase();
_callback = callback;
_isRegex = isRegex;
_regex = isRegex ? Pattern.compile(_query) : null;
_activityRef = new WeakReference<>(activity);
}
// Called for both, file and folder filter
@Override
public boolean accept(File file) {
return isMatching(file, true);
}
// Not called
@Override
public boolean accept(File dir, String name) {
return isMatching(new File(dir, name), true);
}
// In iterateFilesAndDirs, subdirs are only scanned when returning true on it
// But those dirs will also occur in iterator
// Hence call this aagain with alwaysMatchDir=false
public boolean isMatching(File file, boolean alwaysMatchDir) {
if (file.isDirectory()) {
// Do never scan .git directories, lots of files, lots of time
if (file.getName().equals(".git")) {
return false;
}
if (alwaysMatchDir) {
return true;
}
}
String name = file.getName();
file = file.getParentFile();
return _isRegex ? _regex.matcher(name).matches() : name.toLowerCase().contains(_query);
}
@Override
protected void onPreExecute() {
super.onPreExecute();
if (_activityRef.get() != null) {
_snackBar = Snackbar.make(_activityRef.get().findViewById(android.R.id.content), _query + "...", Snackbar.LENGTH_INDEFINITE);
_snackBar.setAction(android.R.string.cancel, (v) -> {
_snackBar.dismiss();
cancel(true);
}).show();
}
}
@Override
protected List<String> doInBackground(Void... voidp) {
List<String> ret = new ArrayList<>();
boolean first = true;
Iterator<File> iter = FileUtils.iterateFilesAndDirs(_searchDir, this, this);
while (iter.hasNext() && !isCancelled()) {
File f = iter.next();
if (first) {
first = false;
if (f.equals(_searchDir)) {
continue;
}
}
if (f.isFile() || (f.isDirectory() && isMatching(f, false))) {
ret.add(f.getAbsolutePath().replace(_searchDir.getAbsolutePath() + "/", ""));
}
}
return ret;
}
@Override
protected void onPostExecute(List<String> ret) {
super.onPostExecute(ret);
if (_snackBar != null) {
_snackBar.dismiss();
}
if (_callback != null) {
try {
_callback.callback(ret);
} catch (Exception ignored) {
}
}
new ActivityUtils(_activityRef.get()).hideSoftKeyboard().freeContextRef();
}
}
}

View File

@ -17,8 +17,11 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.net.Uri;
import android.os.Build;
import android.provider.CalendarContract;
import android.support.annotation.ColorInt;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
@ -47,6 +50,12 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils {
_activity = activity;
}
@Override
public void freeContextRef() {
super.freeContextRef();
_activity = null;
}
//########################
//## Methods
//########################
@ -85,9 +94,11 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils {
}
public void showSnackBar(@StringRes int stringResId, boolean showLong) {
Snackbar.make(_activity.findViewById(android.R.id.content), stringResId,
showLong ? Snackbar.LENGTH_LONG : Snackbar.LENGTH_SHORT).show();
public Snackbar showSnackBar(@StringRes int stringResId, boolean showLong) {
Snackbar s = Snackbar.make(_activity.findViewById(android.R.id.content), stringResId,
showLong ? Snackbar.LENGTH_LONG : Snackbar.LENGTH_SHORT);
s.show();
return s;
}
public void showSnackBar(@StringRes int stringResId, boolean showLong, @StringRes int actionResId, View.OnClickListener listener) {
@ -97,18 +108,58 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils {
.show();
}
public void hideSoftKeyboard() {
InputMethodManager imm = (InputMethodManager) _activity.getSystemService(Activity.INPUT_METHOD_SERVICE);
if (imm != null && _activity.getCurrentFocus() != null && _activity.getCurrentFocus().getWindowToken() != null) {
imm.hideSoftInputFromWindow(_activity.getCurrentFocus().getWindowToken(), 0);
public ActivityUtils setSoftKeyboardVisibile(boolean visible, View... editView) {
final Activity activity = _activity;
if (activity != null) {
final View v = (editView != null && editView.length > 0) ? (editView[0]) : (activity.getCurrentFocus() != null && activity.getCurrentFocus().getWindowToken() != null ? activity.getCurrentFocus() : null);
final InputMethodManager imm = (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE);
if (v != null && imm != null) {
Runnable r = () -> {
if (visible) {
v.requestFocus();
imm.showSoftInput(v, InputMethodManager.SHOW_FORCED);
} else {
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
}
};
r.run();
for (int d : new int[]{100, 350}) {
v.postDelayed(r, d);
}
}
}
return this;
}
public void showSoftKeyboard() {
InputMethodManager imm = (InputMethodManager) _activity.getSystemService(Activity.INPUT_METHOD_SERVICE);
if (imm != null && _activity.getCurrentFocus() != null && _activity.getCurrentFocus().getWindowToken() != null) {
imm.showSoftInput(_activity.getCurrentFocus(), InputMethodManager.SHOW_FORCED);
public ActivityUtils hideSoftKeyboard() {
if (_activity != null) {
InputMethodManager imm = (InputMethodManager) _activity.getSystemService(Activity.INPUT_METHOD_SERVICE);
if (imm != null && _activity.getCurrentFocus() != null && _activity.getCurrentFocus().getWindowToken() != null) {
imm.hideSoftInputFromWindow(_activity.getCurrentFocus().getWindowToken(), 0);
}
}
return this;
}
public ActivityUtils showSoftKeyboard() {
if (_activity != null) {
InputMethodManager imm = (InputMethodManager) _activity.getSystemService(Activity.INPUT_METHOD_SERVICE);
if (imm != null && _activity.getCurrentFocus() != null && _activity.getCurrentFocus().getWindowToken() != null) {
showSoftKeyboard(_activity.getCurrentFocus());
}
}
return this;
}
public ActivityUtils showSoftKeyboard(View textInputView) {
if (_activity != null) {
InputMethodManager imm = (InputMethodManager) _activity.getSystemService(Activity.INPUT_METHOD_SERVICE);
if (imm != null && textInputView != null) {
imm.showSoftInput(textInputView, InputMethodManager.SHOW_FORCED);
}
}
return this;
}
public void showDialogWithHtmlTextView(@StringRes int resTitleId, String html) {
@ -142,7 +193,7 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils {
}
// Toggle with no param, else set visibility according to first bool
public void toggleStatusbarVisibility(boolean... optionalForceVisible) {
public ActivityUtils toggleStatusbarVisibility(boolean... optionalForceVisible) {
WindowManager.LayoutParams attrs = _activity.getWindow().getAttributes();
int flag = WindowManager.LayoutParams.FLAG_FULLSCREEN;
if (optionalForceVisible.length == 0) {
@ -153,9 +204,10 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils {
attrs.flags |= flag;
}
_activity.getWindow().setAttributes(attrs);
return this;
}
public void showGooglePlayEntryForThisApp() {
public ActivityUtils showGooglePlayEntryForThisApp() {
String pkgId = "details?id=" + _activity.getPackageName();
Intent goToMarket = new Intent(Intent.ACTION_VIEW, Uri.parse("market://" + pkgId));
goToMarket.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY |
@ -167,9 +219,10 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils {
_activity.startActivity(new Intent(Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/apps/" + pkgId)));
}
return this;
}
public void setStatusbarColor(int color, boolean... fromRes) {
public ActivityUtils setStatusbarColor(int color, boolean... fromRes) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (fromRes != null && fromRes.length > 0 && fromRes[0]) {
color = ContextCompat.getColor(_context, color);
@ -177,13 +230,55 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils {
_activity.getWindow().setStatusBarColor(color);
}
return this;
}
public void setLauncherActivityEnabled(Class activityClass, boolean enable) {
public ActivityUtils setLauncherActivityEnabled(Class activityClass, boolean enable) {
Context context = _context.getApplicationContext();
PackageManager pkg = context.getPackageManager();
ComponentName component = new ComponentName(context, activityClass);
pkg.setComponentEnabledSetting(component, enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED
, PackageManager.DONT_KILL_APP);
pkg.setComponentEnabledSetting(component, enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
return this;
}
@ColorInt
public Integer getCurrentPrimaryColor() {
TypedValue typedValue = new TypedValue();
_context.getTheme().resolveAttribute(getResId(ResType.ATTR, "colorPrimary"), typedValue, true);
return typedValue.data;
}
@ColorInt
public Integer getCurrentPrimaryDarkColor() {
TypedValue typedValue = new TypedValue();
_context.getTheme().resolveAttribute(getResId(ResType.ATTR, "colorPrimaryDark"), typedValue, true);
return typedValue.data;
}
@ColorInt
public Integer getCurrentAccentColor() {
TypedValue typedValue = new TypedValue();
_context.getTheme().resolveAttribute(getResId(ResType.ATTR, "colorAccent"), typedValue, true);
return typedValue.data;
}
@ColorInt
public Integer getActivityBackgroundColor() {
TypedArray array = _activity.getTheme().obtainStyledAttributes(new int[]{
android.R.attr.colorBackground,
});
int c = array.getColor(0, 0xFF0000);
array.recycle();
return c;
}
public ActivityUtils startCalendarApp() {
Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
builder.appendPath("time");
builder.appendPath(Long.toString(System.currentTimeMillis()));
Intent intent = new Intent(Intent.ACTION_VIEW, builder.build());
_activity.startActivity(intent);
return this;
}
}

View File

@ -31,4 +31,24 @@ public class Callback {
public interface a5<A, B, C, D, E> {
void callback(A arg1, B arg2, C arg3, D arg4, E arg5);
}
public interface b1<A> {
boolean callback(A arg1);
}
public interface b2<A, B> {
boolean callback(A arg1, B arg2);
}
public interface b3<A, B, C> {
boolean callback(A arg1, B arg2, C arg3);
}
public interface b4<A, B, C, D> {
boolean callback(A arg1, B arg2, C arg3, D arg4);
}
public interface b5<A, B, C, D, E> {
boolean callback(A arg1, B arg2, C arg3, D arg4, E arg5);
}
}

View File

@ -38,6 +38,7 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.SystemClock;
import android.support.annotation.ColorInt;
import android.support.annotation.ColorRes;
@ -48,6 +49,7 @@ import android.support.annotation.StringRes;
import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.util.Pair;
import android.text.Html;
import android.text.InputFilter;
import android.text.SpannableString;
@ -74,6 +76,8 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
@ -94,6 +98,9 @@ public class ContextUtils {
return _context;
}
public void freeContextRef() {
_context = null;
}
//
// Class Methods
@ -171,15 +178,23 @@ public class ContextUtils {
return String.format(a ? "#%08X" : "#%06X", (a ? 0xFFFFFFFF : 0xFFFFFF) & intColor);
}
public String getAndroidVersion() {
return Build.VERSION.RELEASE + " (" + Build.VERSION.SDK_INT + ")";
}
public String getAppVersionName() {
PackageManager manager = _context.getPackageManager();
try {
PackageManager manager = _context.getPackageManager();
PackageInfo info = manager.getPackageInfo(getPackageIdManifest(), 0);
return info.versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return "?";
try {
PackageInfo info = manager.getPackageInfo(getPackageIdReal(), 0);
return info.versionName;
} catch (PackageManager.NameNotFoundException ignored) {
}
}
return "?";
}
public String getAppInstallationSource() {
@ -519,12 +534,76 @@ public class ContextUtils {
/**
* Get the private directory for the current package (usually /data/data/package.name/)
*/
public String getAppDataDir() {
@SuppressWarnings("StatementWithEmptyBody")
public File getAppDataPrivateDir() {
File filesDir;
try {
return _context.getPackageManager().getPackageInfo(getPackageIdReal(), 0).applicationInfo.dataDir;
filesDir = new File(new File(_context.getPackageManager().getPackageInfo(getPackageIdReal(), 0).applicationInfo.dataDir), "files");
} catch (PackageManager.NameNotFoundException e) {
return _context.getFilesDir().getParent();
filesDir = _context.getFilesDir();
}
if (!filesDir.exists() && filesDir.mkdirs()) ;
return filesDir;
}
/**
* Get public (accessible) appdata folders
*/
@SuppressWarnings("StatementWithEmptyBody")
public List<Pair<File, String>> getAppDataPublicDirs(boolean internalStorageFolder, boolean sdcardFolders, boolean storageNameWithoutType) {
List<Pair<File, String>> dirs = new ArrayList<>();
for (File externalFileDir : ContextCompat.getExternalFilesDirs(_context, null)) {
if (externalFileDir == null || Environment.getExternalStorageDirectory() == null) {
continue;
}
boolean isInt = externalFileDir.getAbsolutePath().startsWith(Environment.getExternalStorageDirectory().getAbsolutePath());
boolean add = (internalStorageFolder && isInt) || (sdcardFolders && !isInt);
if (add) {
dirs.add(new Pair<>(externalFileDir, getStorageName(externalFileDir, storageNameWithoutType)));
if (!externalFileDir.exists() && externalFileDir.mkdirs()) ;
}
}
return dirs;
}
public String getStorageName(File externalFileDir, boolean storageNameWithoutType) {
boolean isInt = externalFileDir.getAbsolutePath().startsWith(Environment.getExternalStorageDirectory().getAbsolutePath());
String[] split = externalFileDir.getAbsolutePath().split("/");
if (split.length > 2) {
return isInt ? (storageNameWithoutType ? "Internal Storage" : "") : (storageNameWithoutType ? split[2] : ("SD Card (" + split[2] + ")"));
} else {
return "Storage";
}
}
public List<Pair<File, String>> getStorages(boolean internalStorageFolder, boolean sdcardFolders) {
List<Pair<File, String>> storages = new ArrayList<>();
for (Pair<File, String> pair : getAppDataPublicDirs(internalStorageFolder, sdcardFolders, true)) {
if (pair.first != null && pair.first.getAbsolutePath().lastIndexOf("/Android/data") > 0) {
try {
storages.add(new Pair<>(new File(pair.first.getCanonicalPath().replaceFirst("/Android/data.*", "")), pair.second));
} catch (IOException ignored) {
}
}
}
return storages;
}
public File getStorageRootFolder(File file) {
String filepath;
try {
filepath = file.getCanonicalPath();
} catch (Exception ignored) {
return null;
}
for (Pair<File, String> storage : getStorages(false, true)) {
//noinspection ConstantConditions
if (filepath.startsWith(storage.first.getAbsolutePath())) {
return storage.first;
}
}
return null;
}
/**
@ -854,6 +933,18 @@ public class ContextUtils {
}
return mimeType;
}
public Integer parseColor(String colorstr) {
if (colorstr == null || colorstr.trim().isEmpty()) {
return null;
}
try {
return Color.parseColor(colorstr);
} catch (IllegalArgumentException ignored) {
return null;
}
}
}

View File

@ -405,7 +405,7 @@ public class FileUtils {
* Analyze given textfile and retrieve multiple information from it
* Information is written back to the {@link AtomicInteger} parameters
*/
public static void retrieveTextFileSummary(File file, AtomicInteger numCharacters, AtomicInteger numLines) {
public static void retrieveTextFileSummary(File file, AtomicInteger numCharacters, AtomicInteger numLines, AtomicInteger numWords) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(file));
@ -413,11 +413,15 @@ public class FileUtils {
while ((line = br.readLine()) != null) {
numLines.getAndIncrement();
numCharacters.getAndSet(numCharacters.get() + line.length());
if (!line.equals("")) {
numWords.getAndSet(numWords.get() + line.split("\\s+").length);
}
}
} catch (Exception e) {
e.printStackTrace();
numCharacters.set(-1);
numLines.set(-1);
numWords.set(-1);
} finally {
if (br != null) {
try {

View File

@ -10,6 +10,8 @@
#########################################################*/
package net.gsantner.opoc.util;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
@ -37,19 +39,29 @@ import android.provider.MediaStore;
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.annotation.StringRes;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.content.pm.ShortcutInfoCompat;
import android.support.v4.content.pm.ShortcutManagerCompat;
import android.support.v4.graphics.drawable.IconCompat;
import android.support.v4.provider.DocumentFile;
import android.support.v4.util.Pair;
import android.support.v7.app.AlertDialog;
import android.support.v7.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.webkit.WebView;
import android.widget.ImageView;
import android.widget.Toast;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@ -65,15 +77,17 @@ import static android.app.Activity.RESULT_OK;
* Also allows to parse/fetch information out of shared information.
* (M)Permissions are not checked, wrap ShareUtils methods if neccessary
*/
@SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "SameParameterValue", "unused", "deprecation", "ConstantConditions", "ObsoleteSdkInt", "SpellCheckingInspection"})
@SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "SameParameterValue", "unused", "deprecation", "ConstantConditions", "ObsoleteSdkInt", "SpellCheckingInspection", "JavadocReference"})
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());
public final static String MIME_TEXT_PLAIN = "text/plain";
public final static String PREF_KEY__SAF_TREE_URI = "pref_key__saf_tree_uri";
public final static int REQUEST_CAMERA_PICTURE = 50001;
public final static int REQUEST_PICK_PICTURE = 50002;
public final static int REQUEST_SAF = 50003;
protected static String _lastCameraPictureFilepath;
@ -86,6 +100,10 @@ public class ShareUtil {
_chooserTitle = "";
}
public void freeContextRef() {
_context = null;
}
public String getFileProviderAuthority() {
if (TextUtils.isEmpty(_fileProviderAuthority)) {
throw new RuntimeException("Error at ShareUtil.getFileProviderAuthority(): No FileProvider authority provided");
@ -513,6 +531,15 @@ public class ShareUtil {
fileStr = fileStr.substring(prefix.length());
}
}
// external/ prefix for External storage
if (fileStr.startsWith((tmps = "external/"))) {
File f = new File(Uri.decode(Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + fileStr.substring(tmps.length())));
if (f.exists()) {
return f;
}
}
// Next/OwnCloud Fileprovider
for (String fp : new String[]{"org.nextcloud.files", "org.nextcloud.beta.files", "org.owncloud.files"}) {
if (fileProvider.equals(fp) && fileStr.startsWith(tmps = "external_files/")) {
@ -527,6 +554,7 @@ public class ShareUtil {
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));
@ -557,7 +585,11 @@ public class ShareUtil {
throw new RuntimeException("Error: ShareUtil.requestGalleryPicture needs an Activity Context.");
}
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
((Activity) _context).startActivityForResult(intent, REQUEST_PICK_PICTURE);
try {
((Activity) _context).startActivityForResult(intent, REQUEST_PICK_PICTURE);
} catch (Exception ex) {
Toast.makeText(_context, "No gallery app installed!", Toast.LENGTH_SHORT).show();
}
}
/**
@ -565,7 +597,7 @@ public class ShareUtil {
* Result ({@link String}) will be available from {@link Activity#onActivityResult(int, int, Intent)}.
* It has set resultCode to {@link Activity#RESULT_OK} with same requestCode, if successfully
* The requested image savepath has to be stored at caller side (not contained in intent),
* it can be retrieved using {@link #extractResultFromActivityResult(int, int, Intent)},
* it can be retrieved using {@link #extractResultFromActivityResult(int, int, Intent, Activity...)}
* returns null if an error happened.
*
* @param target Path to file to write to, if folder the filename gets app_name + millis + random filename. If null DCIM folder is used.
@ -621,7 +653,9 @@ public class ShareUtil {
* Forward all arguments from activity. Only requestCodes from {@link ShareUtil} get analyzed.
* Also may forward results via local broadcast
*/
public Object extractResultFromActivityResult(int requestCode, int resultCode, Intent data) {
@SuppressLint("ApplySharedPref")
public Object extractResultFromActivityResult(int requestCode, int resultCode, Intent data, Activity... activityOrNull) {
Activity activity = greedyGetActivity(activityOrNull);
switch (requestCode) {
case REQUEST_CAMERA_PICTURE: {
String picturePath = (resultCode == RESULT_OK) ? _lastCameraPictureFilepath : null;
@ -676,6 +710,18 @@ public class ShareUtil {
}
break;
}
case REQUEST_SAF: {
if (resultCode == RESULT_OK && data != null && data.getData() != null) {
Uri treeUri = data.getData();
PreferenceManager.getDefaultSharedPreferences(_context).edit().putString(PREF_KEY__SAF_TREE_URI, treeUri.toString()).commit();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
activity.getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
return treeUri;
}
break;
}
}
return null;
}
@ -822,4 +868,264 @@ public class ShareUtil {
customTabIntent.setPackage(pkg);
}
}
/***
* Request storage access. The user needs to press "Select storage" at the correct storage.
* @param activity The activity which will receive the result from startActivityForResult
*/
public void requestStorageAccessFramework(Activity... activity) {
Activity a = greedyGetActivity(activity);
if (a != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
);
a.startActivityForResult(intent, REQUEST_SAF);
}
}
/**
* Get storage access framework tree uri. The user must have granted access via {@link #requestStorageAccessFramework(Activity...)}
*
* @return Uri or null if not granted yet
*/
public Uri getStorageAccessFrameworkTreeUri() {
String treeStr = PreferenceManager.getDefaultSharedPreferences(_context).getString(PREF_KEY__SAF_TREE_URI, null);
if (!TextUtils.isEmpty(treeStr)) {
try {
return Uri.parse(treeStr);
} catch (Exception ignored) {
}
}
return null;
}
/**
* Get mounted storage folder root (by tree uri). The user must have granted access via {@link #requestStorageAccessFramework(Activity...)}
*
* @return File or null if SD not mounted
*/
public File getStorageAccessFolder() {
Uri safUri = getStorageAccessFrameworkTreeUri();
if (safUri != null) {
String safUriStr = safUri.toString();
ContextUtils cu = new ContextUtils(_context);
for (Pair<File, String> storage : cu.getStorages(false, true)) {
@SuppressWarnings("ConstantConditions") String storageFolderName = storage.first.getName();
if (safUriStr.contains(storageFolderName)) {
return storage.first;
}
}
cu.freeContextRef();
}
return null;
}
/**
* Check whether or not a file is under a storage access folder (external storage / SD)
*
* @param file The file object (file/folder)
* @return Wether or not the file is under storage access folder
*/
public boolean isUnderStorageAccessFolder(File file) {
if (file != null) {
ContextUtils cu = new ContextUtils(_context);
for (Pair<File, String> storage : cu.getStorages(false, true)) {
if (file.getAbsolutePath().startsWith(storage.first.getAbsolutePath())) {
cu.freeContextRef();
return true;
}
}
cu.freeContextRef();
}
return false;
}
/**
* Greedy extract Activity from parameter or convert context if it's a activity
*/
private Activity greedyGetActivity(Activity... activity) {
if (activity != null && activity.length != 0 && activity[0] != null) {
return activity[0];
}
if (_context instanceof Activity) {
return (Activity) _context;
}
return null;
}
/**
* Check whether or not a file can be written.
* Requires storage access framework permission for external storage (SD)
*
* @param file The file object (file/folder)
* @param isDir Wether or not the given file parameter is a directory
* @return Wether or not the file can be written
*/
public boolean canWriteFile(File file, boolean isDir) {
if (file == null) {
return false;
} else if (file.getAbsolutePath().startsWith(Environment.getExternalStorageDirectory().getAbsolutePath())) {
boolean s1 = isDir && file.getParentFile().canWrite();
return !isDir && file.getParentFile() != null ? file.getParentFile().canWrite() : file.canWrite();
} else {
DocumentFile dof = getDocumentFile(file, isDir);
return dof != null && dof.canWrite();
}
}
/**
* Get a {@link DocumentFile} object out of a normal java {@link File}.
* When used on a external storage (SD), use {@link #requestStorageAccessFramework(Activity...)}
* first to get access. Otherwise this will fail.
*
* @param file The file/folder to convert
* @param isDir Wether or not file is a directory. For non-existing (to be created) files this info is not known hence required.
* @return A {@link DocumentFile} object or null if file cannot be converted
*/
public DocumentFile getDocumentFile(File file, boolean isDir) {
// On older versions use fromFile
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
return DocumentFile.fromFile(file);
}
// Get ContextUtils to find storageRootFolder
ContextUtils cu = new ContextUtils(_context);
File baseFolderFile = cu.getStorageRootFolder(file);
cu.freeContextRef();
String baseFolder = baseFolderFile == null ? null : baseFolderFile.getAbsolutePath();
boolean originalDirectory = false;
if (baseFolder == null) {
return null;
}
String relPath = null;
try {
String fullPath = file.getCanonicalPath();
if (!baseFolder.equals(fullPath)) {
relPath = fullPath.substring(baseFolder.length() + 1);
} else {
originalDirectory = true;
}
} catch (IOException e) {
return null;
} catch (Exception ignored) {
originalDirectory = true;
}
Uri treeUri;
if ((treeUri = getStorageAccessFrameworkTreeUri()) == null) {
return null;
}
DocumentFile dof = DocumentFile.fromTreeUri(_context, treeUri);
if (originalDirectory) {
return dof;
}
String[] parts = relPath.split("\\/");
for (int i = 0; i < parts.length; i++) {
DocumentFile nextDof = dof.findFile(parts[i]);
if (nextDof == null) {
nextDof = ((i < parts.length - 1) || isDir) ? dof.createDirectory(parts[i]) : dof.createFile("image", parts[i]);
}
dof = nextDof;
}
return dof;
}
public void showMountSdDialog(@StringRes int title, @StringRes int description, @DrawableRes int mountDescriptionGraphic, Activity... activityOrNull) {
Activity activity = greedyGetActivity(activityOrNull);
if (activity == null) {
return;
}
// Image viewer
ImageView imv = new ImageView(activity);
imv.setImageResource(mountDescriptionGraphic);
imv.setAdjustViewBounds(true);
AlertDialog.Builder dialog = new AlertDialog.Builder(activity);
dialog.setView(imv);
dialog.setTitle(title);
dialog.setMessage(_context.getString(description) + "\n\n");
dialog.setNegativeButton(android.R.string.cancel, null);
dialog.setPositiveButton(android.R.string.yes, (dialogInterface, i) -> requestStorageAccessFramework(activity));
AlertDialog dialogi = dialog.create();
dialogi.show();
}
public void writeFile(File file, boolean isDirectory, Callback.a2<Boolean, FileOutputStream> writeFileCallback) {
try {
FileOutputStream fileOutputStream = null;
ParcelFileDescriptor pfd = null;
if (file.canWrite()) {
if (isDirectory) {
file.mkdirs();
} else {
fileOutputStream = new FileOutputStream(file);
}
} else {
DocumentFile dof = getDocumentFile(file, isDirectory);
if (dof != null && dof.getUri() != null && dof.canWrite()) {
if (isDirectory) {
// Nothing to do
} else {
pfd = _context.getContentResolver().openFileDescriptor(dof.getUri(), "w");
fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
}
}
}
if (writeFileCallback != null) {
writeFileCallback.callback(fileOutputStream != null || (isDirectory && file.exists()), fileOutputStream);
}
if (fileOutputStream != null) {
fileOutputStream.close();
}
if (pfd != null) {
pfd.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Call telephone number.
* Non direct call, opens up the dialer and pre-sets the telephone number. User needs to press manually.
* Direct call requires M permission granted, also add permissions to manifest:
* <uses-permission android:name="android.permission.CALL_PHONE" />
*
* @param telNo The telephone number to call
* @param directCall Direct call number if possible
*/
@SuppressWarnings("SimplifiableConditionalExpression")
public void callTelephoneNumber(String telNo, boolean... directCall) {
Activity activity = greedyGetActivity();
if (activity == null) {
throw new RuntimeException("Error: ShareUtil::callTelephoneNumber needs to be contstructed with activity context");
}
boolean ldirectCall = (directCall != null && directCall.length > 0) ? directCall[0] : true;
if (android.os.Build.VERSION.SDK_INT >= 23 && ldirectCall && activity != null) {
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.CALL_PHONE}, 4001);
ldirectCall = false;
} else {
try {
Intent callIntent = new Intent(Intent.ACTION_CALL);
callIntent.setData(Uri.parse("tel:" + telNo));
activity.startActivity(callIntent);
} catch (Exception ignored) {
ldirectCall = false;
}
}
}
// Show dialer up with telephone number pre-inserted
if (!ldirectCall) {
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.fromParts("tel", telNo, null));
activity.startActivity(intent);
}
}
}

View File

@ -13,8 +13,8 @@ import java.text.SimpleDateFormat
buildscript {
ext {
version_gradle_tools = "3.2.1"
version_plugin_kotlin = "1.3.11"
version_gradle_tools = "3.4.2"
version_plugin_kotlin = "1.3.41"
enable_plugin_kotlin = false
version_compileSdk = 28
@ -79,7 +79,7 @@ static String findUsedAndroidLocales() {
Set<String> langs = new HashSet<>()
new File('.').eachFileRecurse(groovy.io.FileType.DIRECTORIES) {
final foldername = it.name
if (foldername.startsWith('values-') && !it.canonicalPath.contains("build" + File.separator + "intermediates")) {
if (foldername.startsWith('values-') && !it.canonicalPath.contains("build" + File.separator + "intermediates") && !it.canonicalPath.contains("gradle" + File.separator + "daemon")) {
new File(it.toString()).eachFileRecurse(groovy.io.FileType.FILES) {
if (it.name.toLowerCase().endsWith(".xml") && it.getCanonicalFile().getText('UTF-8').contains("<string")) {
langs.add(foldername.replace("values-", ""))

View File

@ -1,5 +1,6 @@
#Fri Jul 26 02:59:10 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.5.1-all.zip