diff --git a/app/build.gradle b/app/build.gradle index c65168d7..0cd879c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,7 +60,7 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10' - implementation 'com.google.android.material:material:1.1.0' + implementation 'com.google.android.material:material:1.2.0' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.palette:palette:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' @@ -71,7 +71,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "androidx.work:work-runtime-ktx:2.4.0" implementation "androidx.fragment:fragment-ktx:1.2.3" - + implementation "androidx.browser:browser:1.2.0" testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' diff --git a/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java b/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java index 9e09bd2e..244f9afd 100644 --- a/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java +++ b/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java @@ -29,7 +29,7 @@ public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provi SoLoader.init(this, false); initFlipper(); - initNiddler(); + //initNiddler(); } private void initFlipper() { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 13390561..9d47cb49 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,9 @@ - + { - final int DRAWABLE_RIGHT = 2; - - int drawablePos = (binding.addFeedTextInput.getRight() - - binding.addFeedTextInput.getCompoundDrawables()[DRAWABLE_RIGHT].getBounds().width()); - if (event.getAction() == MotionEvent.ACTION_UP && event.getRawX() >= drawablePos) { - binding.addFeedTextInput.setText(""); - return true; - } - - return false; - }); - viewModel = new ViewModelProvider(this).get(AddFeedsViewModel.class); parseItemsAdapter = new ItemAdapter<>(); fastAdapter = FastAdapter.with(parseItemsAdapter); fastAdapter.withSelectable(true); fastAdapter.withOnClickListener((v, adapter, item, position) -> { - if (item.isChecked()) { - item.setChecked(false); - fastAdapter.notifyAdapterItemChanged(position); - } else { - item.setChecked(true); - fastAdapter.notifyAdapterItemChanged(position); - } - + item.setChecked(!item.isChecked()); + + fastAdapter.notifyAdapterItemChanged(position); binding.addFeedOk.setEnabled(recyclerViewHasCheckedItems()); return true; diff --git a/app/src/main/java/com/readrops/app/activities/ItemActivity.java b/app/src/main/java/com/readrops/app/activities/ItemActivity.java index a1493f25..83e56894 100644 --- a/app/src/main/java/com/readrops/app/activities/ItemActivity.java +++ b/app/src/main/java/com/readrops/app/activities/ItemActivity.java @@ -22,6 +22,7 @@ import android.webkit.WebView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.browser.customtabs.CustomTabsIntent; import androidx.core.app.ActivityCompat; import androidx.core.app.ShareCompat; import androidx.lifecycle.ViewModelProvider; @@ -199,12 +200,7 @@ public class ItemActivity extends AppCompatActivity { shareArticle(); return true; case R.id.item_open: - int value = Integer.parseInt(SharedPreferencesManager.readString(this, - SharedPreferencesManager.SharedPrefKey.OPEN_ITEMS_IN)); - if (value == 0) - openInNavigator(); - else - openInWebView(); + openUrl(); return true; default: return super.onOptionsItemSelected(item); @@ -217,6 +213,22 @@ public class ItemActivity extends AppCompatActivity { super.onBackPressed(); } + private void openUrl() { + int value = Integer.parseInt(SharedPreferencesManager.readString(this, + SharedPreferencesManager.SharedPrefKey.OPEN_ITEMS_IN)); + switch (value) { + case 0: + openInNavigator(); + break; + case 1: + openInWebView(); + break; + default: + openInCustomTab(); + break; + } + } + private void openInNavigator() { Intent urlIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(itemWithFeed.getItem().getLink())); startActivity(urlIntent); @@ -225,11 +237,27 @@ public class ItemActivity extends AppCompatActivity { private void openInWebView() { Intent intent = new Intent(this, WebViewActivity.class); intent.putExtra(WEB_URL, itemWithFeed.getItem().getLink()); - intent.putExtra(ACTION_BAR_COLOR, itemWithFeed.getColor() != 0 ? itemWithFeed.getColor() : itemWithFeed.getBgColor()); + intent.putExtra(ACTION_BAR_COLOR, itemWithFeed.getBgColor() != 0 ? itemWithFeed.getBgColor() : itemWithFeed.getColor()); startActivity(intent); } + private void openInCustomTab() { + boolean darkTheme = Boolean.parseBoolean(SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.DARK_THEME)); + int color = itemWithFeed.getBgColor() != 0 ? itemWithFeed.getBgColor() : itemWithFeed.getColor(); + + CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder() + .addDefaultShareMenuItem() + .setToolbarColor(color) + .setSecondaryToolbarColor(color) + .setColorScheme(darkTheme ? CustomTabsIntent.COLOR_SCHEME_DARK : CustomTabsIntent.COLOR_SCHEME_LIGHT) + .enableUrlBarHiding() + .setShowTitle(true) + .build(); + + customTabsIntent.launchUrl(this, Uri.parse(itemWithFeed.getItem().getLink())); + } + private void shareArticle() { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); diff --git a/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java b/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java index 2df601a6..71f8867b 100644 --- a/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java +++ b/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java @@ -7,10 +7,9 @@ import android.app.PendingIntent; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; +import android.os.Build; import android.os.Bundle; -import android.os.Environment; import android.provider.Settings; -import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -29,6 +28,7 @@ import com.readrops.app.ReadropsApp; import com.readrops.app.activities.AddAccountActivity; import com.readrops.app.activities.ManageFeedsFoldersActivity; import com.readrops.app.activities.NotificationPermissionActivity; +import com.readrops.app.utils.FileUtils; import com.readrops.app.utils.PermissionManager; import com.readrops.app.utils.SharedPreferencesManager; import com.readrops.app.utils.Utils; @@ -36,14 +36,10 @@ import com.readrops.app.viewmodels.AccountViewModel; import com.readrops.db.entities.account.Account; import com.readrops.db.entities.account.AccountType; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; - import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.observers.DisposableCompletableObserver; import io.reactivex.schedulers.Schedulers; +import kotlin.Unit; import static android.app.Activity.RESULT_OK; import static com.readrops.api.opml.OPMLHelper.OPEN_OPML_FILE_REQUEST; @@ -77,6 +73,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { return fragment; } + @SuppressWarnings("ConstantConditions") @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { addPreferencesFromResource(R.xml.acount_preferences); @@ -121,16 +118,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { opmlPref.setOnPreferenceClickListener(preference -> { new MaterialDialog.Builder(getActivity()) .items(R.array.opml_import_export) - .itemsCallback(((dialog, itemView, position, text) -> { - if (position == 0) { - OPMLHelper.openFileIntent(this); - } else { - if (PermissionManager.isPermissionGranted(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) - exportAsOPMLFile(); - else - requestExternalStoragePermission(); - } - })) + .itemsCallback(((dialog, itemView, position, text) -> openOPMLMode(position))) .show(); return true; }); @@ -179,8 +167,24 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { .show(); } + private void openOPMLMode(int position) { + if (position == 0) { + OPMLHelper.openFileIntent(this); + } else { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + if (PermissionManager.isPermissionGranted(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + exportAsOPMLFile(); + } else { + requestExternalStoragePermission(); + } + } else { + exportAsOPMLFile(); + } + } + } + // region opml import - + @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (requestCode == OPEN_OPML_FILE_REQUEST && resultCode == RESULT_OK && data != null) { @@ -231,51 +235,34 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { //region opml export private void exportAsOPMLFile() { + String fileName = "subscriptions.opml"; + try { - String filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(); - File file = new File(filePath, "subscriptions.opml"); + String path = FileUtils.writeDownloadFile(getContext(), fileName, "text/xml", outputStream -> { + viewModel.getFoldersWithFeeds() + .flatMapCompletable(folderListMap -> OPMLParser.write(folderListMap, outputStream)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnError(e -> Utils.showSnackbar(getView(), e.getMessage())) + .subscribe(); - final OutputStream outputStream = new FileOutputStream(file); + return Unit.INSTANCE; + }); - viewModel.getFoldersWithFeeds() - .flatMapCompletable(folderListMap -> OPMLParser.write(folderListMap, outputStream)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doAfterTerminate(() -> { - try { - outputStream.flush(); - outputStream.close(); - - } catch (IOException e) { - Log.e(TAG, e.getMessage()); - Utils.showSnackbar(getView(), e.getMessage()); - } - }) - .subscribe(new DisposableCompletableObserver() { - @Override - public void onComplete() { - displayNotification(file); - } - - @Override - public void onError(Throwable e) { - Utils.showSnackbar(getView(), e.getMessage()); - } - }); + displayNotification(fileName, path); } catch (Exception e) { - Log.e(TAG, e.getMessage()); - Utils.showSnackbar(getView(), e.getMessage()); + displayErrorMessage(); } } - private void displayNotification(File file) { + private void displayNotification(String name, String absolutePath) { Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(Uri.parse(file.getAbsolutePath()), "text/plain"); + intent.setDataAndType(Uri.parse(absolutePath), "text/plain"); Notification notification = new NotificationCompat.Builder(getContext(), ReadropsApp.OPML_EXPORT_CHANNEL_ID) .setContentTitle(getString(R.string.opml_export)) - .setContentText(file.getName()) + .setContentText(name) .setSmallIcon(R.drawable.ic_notif) .setContentIntent(PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)) .setAutoCancel(true) diff --git a/app/src/main/java/com/readrops/app/utils/FileUtils.kt b/app/src/main/java/com/readrops/app/utils/FileUtils.kt new file mode 100644 index 00000000..9ae24dff --- /dev/null +++ b/app/src/main/java/com/readrops/app/utils/FileUtils.kt @@ -0,0 +1,69 @@ +package com.readrops.app.utils + +import android.content.ContentValues +import android.content.Context +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import androidx.annotation.RequiresApi +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream + +object FileUtils { + + @JvmStatic + fun writeDownloadFile(context: Context, fileName: String, mimeType: String, listener: (OutputStream) -> Unit): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + writeFileApi29(context, fileName, mimeType, listener) + else + writeFileApi28(fileName, listener) + } + + @RequiresApi(Build.VERSION_CODES.Q) + private fun writeFileApi29(context: Context, fileName: String, mimeType: String, listener: (OutputStream) -> Unit): String { + val resolver = context.contentResolver + val downloadsUri = MediaStore.Downloads + .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + + val fileDetails = ContentValues().apply { + put(MediaStore.Downloads.DISPLAY_NAME, fileName) + put(MediaStore.Downloads.IS_PENDING, 1) + put(MediaStore.Downloads.MIME_TYPE, mimeType) + } + + val contentUri = resolver.insert(downloadsUri, fileDetails) + + resolver.openFileDescriptor(contentUri!!, "w", null).use { pfd -> + val outputStream = FileOutputStream(pfd?.fileDescriptor!!) + + try { + listener(outputStream) + } catch (e: Exception) { + throw e + } finally { + outputStream.flush() + outputStream.close() + } + } + + fileDetails.clear() + fileDetails.put(MediaStore.Downloads.IS_PENDING, 0) + resolver.update(contentUri, fileDetails, null, null) + + return contentUri.path!! + } + + private fun writeFileApi28(fileName: String, listener: (OutputStream) -> Unit): String { + val filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + val file = File(filePath, fileName) + + val outputStream = FileOutputStream(file) + listener(outputStream) + + outputStream.flush() + outputStream.close() + + return file.absolutePath + } +} \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/utils/GlideModule.java b/app/src/main/java/com/readrops/app/utils/GlideModule.java deleted file mode 100644 index 7d765a40..00000000 --- a/app/src/main/java/com/readrops/app/utils/GlideModule.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.readrops.app.utils; - -import androidx.annotation.NonNull; - -import com.bumptech.glide.module.AppGlideModule; - -@com.bumptech.glide.annotation.GlideModule -public class GlideModule extends AppGlideModule { - -} diff --git a/app/src/main/java/com/readrops/app/utils/HtmlParser.java b/app/src/main/java/com/readrops/app/utils/HtmlParser.java index c1f5b7d2..85c7965d 100644 --- a/app/src/main/java/com/readrops/app/utils/HtmlParser.java +++ b/app/src/main/java/com/readrops/app/utils/HtmlParser.java @@ -15,6 +15,7 @@ import org.jsoup.select.Elements; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.regex.Pattern; @@ -33,25 +34,30 @@ public final class HtmlParser { * @param url url to request * @return a list of rss urls with their title */ - public static List getFeedLink(String url) throws Exception { + public static List getFeedLink(String url) { List results = new ArrayList<>(); - Document document = Jsoup.parse(getHTMLHeadFromUrl(url), url); + String head = getHTMLHeadFromUrl(url); + if (head != null) { + Document document = Jsoup.parse(head, url); - Elements elements = document.select("link"); + Elements elements = document.select("link"); - for (Element element : elements) { - String type = element.attributes().get("type"); + for (Element element : elements) { + String type = element.attributes().get("type"); - if (isTypeRssFeed(type)) { - String feedUrl = element.absUrl("href"); - String label = element.attributes().get("title"); + if (isTypeRssFeed(type)) { + String feedUrl = element.absUrl("href"); + String label = element.attributes().get("title"); - results.add(new ParsingResult(feedUrl, label)); + results.add(new ParsingResult(feedUrl, label)); + } } - } - return results; + return results; + } else { + return Collections.emptyList(); + } } private static boolean isTypeRssFeed(String type) { diff --git a/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt b/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt new file mode 100644 index 00000000..3138c7dd --- /dev/null +++ b/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt @@ -0,0 +1,21 @@ +package com.readrops.app.utils + +import android.content.Context +import com.bumptech.glide.Glide +import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader +import com.bumptech.glide.load.model.GlideUrl +import com.bumptech.glide.module.AppGlideModule +import com.readrops.api.utils.HttpManager +import java.io.InputStream + +@GlideModule +class ReadropsGlideModule : AppGlideModule() { + + override fun registerComponents(context: Context, glide: Glide, registry: Registry) { + val factory = OkHttpUrlLoader.Factory(HttpManager.getInstance().okHttpClient) + + glide.registry.replace(GlideUrl::class.java, InputStream::class.java, factory) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_add_feed.xml b/app/src/main/res/layout/activity_add_feed.xml index 123511a4..cd53baf3 100644 --- a/app/src/main/res/layout/activity_add_feed.xml +++ b/app/src/main/res/layout/activity_add_feed.xml @@ -36,6 +36,7 @@ android:id="@+id/add_feed_input_layout" android:layout_width="0dp" android:layout_height="wrap_content" + app:endIconMode="clear_text" app:layout_constraintEnd_toStartOf="@id/add_feed_load" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -44,7 +45,6 @@ android:id="@+id/add_feed_text_input" android:layout_width="match_parent" android:layout_height="wrap_content" - android:drawableEnd="@drawable/ic_cancel_grey" android:hint="@string/feed_url" android:inputType="text" /> diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index fe4f52b0..cc89f85b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -132,5 +132,6 @@ Afficher la légende Votre mot de passe d\'API (Configuration > Profil) Synchroniser + Vue navigateur \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 824cd35c..a603ebf8 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -33,11 +33,13 @@ @string/external_navigator @string/webview + @string/navigator_view 0 1 + 2 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 077eaec1..8021181f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -138,4 +138,5 @@ Back Show caption Synchronize + Navigator view diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 9a14e305..625fc43a 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -16,7 +16,7 @@ android:title="@string/reload_feeds_colors" />