diff --git a/app/build.gradle b/app/build.gradle index 8f3688b1..beab3f7e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,44 +6,25 @@ if (enable_plugin_kotlin) { } android { - compileSdkVersion version_setup_compileSdk - flavorDimensions "default" + buildToolsVersion rootProject.ext.version_buildTools + compileSdkVersion rootProject.ext.version_compileSdk defaultConfig { - minSdkVersion version_setup_minSdk - targetSdkVersion version_setup_targetSdk + minSdkVersion rootProject.ext.version_minSdk + targetSdkVersion rootProject.ext.version_compileSdk buildConfigField "boolean", "IS_TEST_BUILD", "false" buildConfigField "boolean", "IS_GPLAY_BUILD", "false" buildConfigField "String[]", "DETECTED_ANDROID_LOCALES", "${findUsedAndroidLocales()}" buildConfigField "String", "GITHASH", "\"${getGitHash()}\"" - resValue "string", "manifest_package_id", "com.github.dfa.diaspora_android" + resValue "string", "manifest_package_id", "com.github.dfa.diaspora_android" applicationId "com.github.dfa.diaspora_android" versionName "1.2.3" versionCode 36 - - vectorDrawables.useSupportLibrary = true } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - - sourceSets { - if (enable_plugin_kotlin) { - main.java.srcDirs += 'src/main/kotlin' - } - } - + flavorDimensions "default" productFlavors { flavorDefault { } @@ -64,8 +45,52 @@ android { buildConfigField "boolean", "IS_TEST_BUILD", "true" } } + + sourceSets { + main { assets.srcDirs = ['src/main/assets'] } + if (enable_plugin_kotlin) { + main.java.srcDirs += 'src/main/kotlin' + } + main.java.srcDirs += 'thirdparty/java' + main.res.srcDirs += 'thirdparty/res' + main.assets.srcDirs += 'thirdparty/assets' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + configurations.all { + resolutionStrategy { + eachDependency { details -> + if (details.requested.group == 'com.android.support') { + if (details.requested.name != 'multidex' && details.requested.name != 'multidex-instrumentation') { + details.useVersion "${rootProject.ext.version_library_appcompat}" + } + } + } + } + } + + packagingOptions { + exclude 'META-INF/LICENSE-LGPL-2.1.txt' + exclude 'META-INF/LICENSE-LGPL-3.txt' + exclude 'META-INF/LICENSE-W3C-TEST' + } + + compileOptions { + encoding = 'UTF-8' + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + lintOptions { disable 'MissingTranslation' + disable 'InvalidPackage' + abortOnError false } } diff --git a/app/src/main/java/net/gsantner/opoc/preference/SharedPreferencesPropertyBackend.java b/app/src/main/java/net/gsantner/opoc/preference/SharedPreferencesPropertyBackend.java index 2d1edbd9..665f1058 100644 --- a/app/src/main/java/net/gsantner/opoc/preference/SharedPreferencesPropertyBackend.java +++ b/app/src/main/java/net/gsantner/opoc/preference/SharedPreferencesPropertyBackend.java @@ -164,6 +164,14 @@ public class SharedPreferencesPropertyBackend implements PropertyBackend getStringList(@StringRes int keyResourceId, final SharedPreferences... pref) { return getStringListOne(rstr(keyResourceId), gp(pref)); } diff --git a/app/src/main/java/net/gsantner/opoc/util/ActivityUtils.java b/app/src/main/java/net/gsantner/opoc/util/ActivityUtils.java index 23a9212d..b7d60918 100644 --- a/app/src/main/java/net/gsantner/opoc/util/ActivityUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/ActivityUtils.java @@ -32,6 +32,7 @@ import android.view.View; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import android.webkit.WebView; +import android.widget.ScrollView; @SuppressWarnings({"WeakerAccess", "unused", "SameParameterValue", "SpellCheckingInspection"}) @@ -115,18 +116,18 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils { } public void showDialogWithHtmlTextView(@StringRes int resTitleId, String text, boolean isHtml, DialogInterface.OnDismissListener dismissedListener) { + ScrollView scroll = new ScrollView(_context); AppCompatTextView textView = new AppCompatTextView(_context); - int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, - _context.getResources().getDisplayMetrics()); - textView.setMovementMethod(new LinkMovementMethod()); - textView.setPadding(padding, 0, padding, 0); + int padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, _context.getResources().getDisplayMetrics()); + scroll.setPadding(padding, 0, padding, 0); + scroll.addView(textView); + textView.setMovementMethod(new LinkMovementMethod()); textView.setText(isHtml ? new SpannableString(Html.fromHtml(text)) : text); + AlertDialog.Builder dialog = new AlertDialog.Builder(_context) - .setPositiveButton(android.R.string.ok, null) - .setOnDismissListener(dismissedListener) - .setTitle(resTitleId) - .setView(textView); + .setPositiveButton(android.R.string.ok, null).setOnDismissListener(dismissedListener) + .setTitle(resTitleId).setView(scroll); dialog.show(); } @@ -164,7 +165,7 @@ public class ActivityUtils extends net.gsantner.opoc.util.ContextUtils { _activity.startActivity(goToMarket); } catch (ActivityNotFoundException e) { _activity.startActivity(new Intent(Intent.ACTION_VIEW, - Uri.parse("http://play.google.com/store/apps/" + pkgId))); + Uri.parse("https://play.google.com/store/apps/" + pkgId))); } } diff --git a/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java b/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java index ed90becb..8e7fae98 100644 --- a/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java +++ b/app/src/main/java/net/gsantner/opoc/util/ContextUtils.java @@ -15,6 +15,7 @@ import android.app.Activity; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.ActivityNotFoundException; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; @@ -37,6 +38,7 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; +import android.os.SystemClock; import android.support.annotation.ColorInt; import android.support.annotation.ColorRes; import android.support.annotation.DrawableRes; @@ -47,6 +49,7 @@ import android.support.graphics.drawable.VectorDrawableCompat; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; import android.text.Html; +import android.text.InputFilter; import android.text.SpannableString; import android.text.Spanned; import android.text.TextUtils; @@ -55,6 +58,9 @@ import android.util.DisplayMetrics; import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.webkit.MimeTypeMap; import android.widget.ImageView; import android.widget.TextView; @@ -67,6 +73,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.lang.reflect.Method; +import java.text.SimpleDateFormat; import java.util.Locale; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; @@ -235,7 +242,7 @@ public class ContextUtils { * Get this apps package name, returns the flavor specific package name. */ public String getPackageIdReal() { - return _context.getPackageName(); + return _context.getPackageName(); } /** @@ -509,6 +516,17 @@ public class ContextUtils { return dp * _context.getResources().getDisplayMetrics().density; } + /** + * Get the private directory for the current package (usually /data/data/package.name/) + */ + public String getAppDataDir() { + try { + return _context.getPackageManager().getPackageInfo(getPackageIdReal(), 0).applicationInfo.dataDir; + } catch (PackageManager.NameNotFoundException e) { + return _context.getFilesDir().getParent(); + } + } + /** * Request the givens paths to be scanned by MediaScanner * @@ -694,9 +712,13 @@ public class ContextUtils { public void tintMenuItems(Menu menu, boolean recurse, @ColorInt int iconColor) { for (int i = 0; i < menu.size(); i++) { MenuItem item = menu.getItem(i); - tintDrawable(item.getIcon(), iconColor); - if (item.hasSubMenu() && recurse) { - tintMenuItems(item.getSubMenu(), recurse, iconColor); + try { + tintDrawable(item.getIcon(), iconColor); + if (item.hasSubMenu() && recurse) { + tintMenuItems(item.getSubMenu(), recurse, iconColor); + } + } catch (Exception ignored) { + // This should not happen at all, but may in bad menu.xml configuration } } } @@ -734,4 +756,104 @@ public class ContextUtils { } } } + + + public String getLocalizedDateFormat() { + return ((SimpleDateFormat) android.text.format.DateFormat.getDateFormat(_context)).toPattern(); + } + + public String getLocalizedTimeFormat() { + return ((SimpleDateFormat) android.text.format.DateFormat.getTimeFormat(_context)).toPattern(); + } + + public String getLocalizedDateTimeFormat() { + return getLocalizedDateFormat() + " " + getLocalizedTimeFormat(); + } + + /** + * A {@link InputFilter} for filenames + */ + @SuppressWarnings("Convert2Lambda") + public static final InputFilter INPUTFILTER_FILENAME = new InputFilter() { + public CharSequence filter(CharSequence src, int start, int end, Spanned dest, int dstart, int dend) { + if (src.length() < 1) return null; + char last = src.charAt(src.length() - 1); + String illegal = "|\\?*<\":>+[]/'"; + if (illegal.indexOf(last) > -1) return src.subSequence(0, src.length() - 1); + return null; + } + }; + + /** + * A simple {@link Runnable} which does a touch event on a view. + * This pops up e.g. the keyboard on a {@link android.widget.EditText} + *

+ * Example: new Handler().postDelayed(new DoTouchView(editView), 200); + */ + public static class DoTouchView implements Runnable { + View _view; + + public DoTouchView(View view) { + _view = view; + } + + @Override + public void run() { + _view.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 0, 0, 0)); + _view.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 0, 0, 0)); + } + } + + + public String getMimeType(File file) { + return getMimeType(Uri.fromFile(file)); + } + + /** + * Detect MimeType of given file + * Android/Java's own MimeType map is very very small and detection barely works at all + * Hence use custom map for some file extensions + */ + public String getMimeType(Uri uri) { + String mimeType = null; + if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + ContentResolver cr = _context.getContentResolver(); + mimeType = cr.getType(uri); + } else { + String ext = MimeTypeMap.getFileExtensionFromUrl(uri.toString()); + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.toLowerCase()); + + // Try to guess if the recommended methods fail + if (TextUtils.isEmpty(mimeType)) { + switch (ext) { + case "md": + case "markdown": + case "mkd": + case "mdown": + case "mkdn": + case "mdwn": + case "rmd": + mimeType = "text/markdown"; + break; + case "yaml": + case "yml": + mimeType = "text/yaml"; + break; + case "json": + mimeType = "text/json"; + break; + case "txt": + mimeType = "text/plain"; + break; + } + } + } + + if (TextUtils.isEmpty(mimeType)) { + mimeType = "*/*"; + } + return mimeType; + } } + + diff --git a/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java b/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java index 4b4368cf..13c2589c 100644 --- a/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java +++ b/app/src/main/java/net/gsantner/opoc/util/ShareUtil.java @@ -11,6 +11,7 @@ package net.gsantner.opoc.util; import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.Context; @@ -210,7 +211,7 @@ public class ShareUtil { /** * Start calendar application to add new event, with given details prefilled */ - public void createCalendarAppointment(@Nullable String title, @Nullable String description, @Nullable String location, @Nullable Long... startAndEndTime) { + public boolean createCalendarAppointment(@Nullable String title, @Nullable String description, @Nullable String location, @Nullable Long... startAndEndTime) { Intent intent = new Intent(Intent.ACTION_INSERT).setData(CalendarContract.Events.CONTENT_URI); if (title != null) { intent.putExtra(CalendarContract.Events.TITLE, title); @@ -230,7 +231,13 @@ public class ShareUtil { intent.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, startAndEndTime[1]); } } - _context.startActivity(intent); + + try { + _context.startActivity(intent); + return true; + } catch (ActivityNotFoundException e) { + return false; + } } /** @@ -762,4 +769,57 @@ public class ShareUtil { } return null; } + + /** + * By default Chrome Custom Tabs only uses Chrome Stable to open links + * There are also other packages (like Chrome Beta, Chromium, Firefox, ..) + * which implement the Chrome Custom Tab interface. This method changes + * the customtab intent to use an available compatible browser, if available. + */ + public void enableChromeCustomTabsForOtherBrowsers(Intent customTabIntent) { + String[] checkpkgs = new String[]{ + "com.android.chrome", "com.chrome.beta", "com.chrome.dev", "com.google.android.apps.chrome", "org.chromium.chrome", + "org.mozilla.fennec_fdroid", "org.mozilla.firefox", "org.mozilla.firefox_beta", "org.mozilla.fennec_aurora", + "org.mozilla.klar", "org.mozilla.focus", + }; + + // Get all intent handlers for web links + PackageManager pm = _context.getPackageManager(); + Intent urlIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.example.com")); + List browsers = new ArrayList<>(); + for (ResolveInfo ri : pm.queryIntentActivities(urlIntent, 0)) { + Intent i = new Intent("android.support.customtabs.action.CustomTabsService"); + i.setPackage(ri.activityInfo.packageName); + if (pm.resolveService(i, 0) != null) { + browsers.add(ri.activityInfo.packageName); + } + } + + // Check if the user has a "default browser" selected + ResolveInfo ri = pm.resolveActivity(urlIntent, 0); + String userDefaultBrowser = (ri == null) ? null : ri.activityInfo.packageName; + + // Select which browser to use out of all installed customtab supporting browsers + String pkg = null; + if (browsers.isEmpty()) { + pkg = null; + } else if (browsers.size() == 1) { + pkg = browsers.get(0); + } else if (!TextUtils.isEmpty(userDefaultBrowser) && browsers.contains(userDefaultBrowser)) { + pkg = userDefaultBrowser; + } else { + for (String checkpkg : checkpkgs) { + if (browsers.contains(checkpkg)) { + pkg = checkpkg; + break; + } + } + if (pkg == null && !browsers.isEmpty()) { + pkg = browsers.get(0); + } + } + if (pkg != null && customTabIntent != null) { + customTabIntent.setPackage(pkg); + } + } } diff --git a/build.gradle b/build.gradle index 937b7fc1..25e4f2b8 100644 --- a/build.gradle +++ b/build.gradle @@ -7,37 +7,40 @@ * https://www.apache.org/licenses/LICENSE-2.0 * #########################################################*/ -import java.text.SimpleDateFormat - // Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - ext.version_setup_compileSdk = 27 - ext.version_setup_minSdk = 17 - ext.version_setup_targetSdk = ext.version_setup_compileSdk - ext.version_setup_buildTools = "27.0.3" // Specifying optional +import java.text.SimpleDateFormat - // https://developer.android.com/studio/releases/gradle-plugin.html - ext.version_gradle_tools = "3.1.0" - // https://developer.android.com/topic/libraries/support-library/revisions.html - ext.version_library_appcompat = "27.1.1" - // https://github.com/JakeWharton/butterknife/releases - ext.version_library_butterknife = "8.8.1" - // https://github.com/guardianproject/NetCipher/releases - ext.version_library_netcipher = "2.0.0-alpha1" - // https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-gradle-plugin#LookAtCentral - ext.version_plugin_kotlin = "1.2.21" - ext.enable_plugin_kotlin = false +buildscript { + ext { + version_gradle_tools = "3.2.1" + version_plugin_kotlin = "1.3.11" + enable_plugin_kotlin = false + + version_compileSdk = 28 + version_buildTools = "28.0.3" + version_minSdk = 17 + + // https://developer.android.com/topic/libraries/support-library/ + version_library_appcompat = "28.0.0" //androidx + // https://github.com/JakeWharton/butterknife/releases + version_library_butterknife = "8.8.1" //9.0.0-rc2 + // https://github.com/guardianproject/NetCipher/releases + version_library_netcipher = "2.0.0-alpha1" + } repositories { - google() + maven { url 'https://maven.google.com' } jcenter() + maven { url "https://jitpack.io" } + mavenCentral() } + dependencies { - classpath "com.android.tools.build:gradle:$version_gradle_tools" - classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' + classpath "com.android.tools.build:gradle:${version_gradle_tools}" + classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' if (project.enable_plugin_kotlin) { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$version_plugin_kotlin" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${version_plugin_kotlin}" } // NOTE: Do not place your application dependencies here; they belong @@ -47,10 +50,10 @@ buildscript { allprojects { repositories { - google() + maven { url 'https://maven.google.com' } jcenter() - mavenCentral() maven { url "https://jitpack.io" } + mavenCentral() } tasks.matching { task -> task.name.matches('.*generate.*Resources') }.all { @@ -99,6 +102,7 @@ ext.getGitHash = { -> return 'unknown' } } + @SuppressWarnings(["UnnecessaryQualifiedReference", "SpellCheckingInspection", "GroovyUnusedDeclaration"]) // Returns the build date in a RFC3339 compatible format. TZ is always converted to UTC static String getBuildDate() { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 18a0cf95..0682df5e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Sun Apr 08 08:39:15 CEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-all.zip