From d44d41cdf74c95046c3a171566269b4ee0024e67 Mon Sep 17 00:00:00 2001 From: David Luhmer Date: Sun, 28 Oct 2018 12:44:30 +0100 Subject: [PATCH 1/3] Add support to download full articles for offline viewing (using WebArchive Format) - ref #596 --- .../tests/DownloadWebPageServiceTest.java | 92 +++++ .../luhmer/owncloudnewsreader/Constants.java | 8 +- .../NewsDetailFragment.java | 49 ++- .../NewsReaderListActivity.java | 46 ++- .../owncloudnewsreader/SettingsActivity.java | 8 +- .../database/DatabaseConnectionOrm.java | 4 + .../helper/NotificationActionReceiver.java | 22 ++ .../NextcloudNotificationManager.java | 17 + .../services/DownloadWebPageService.java | 357 ++++++++++++++++++ .../events/StopWebArchiveDownloadEvent.java | 4 + .../main/res/layout/fragment_news_detail.xml | 14 +- .../src/main/res/menu/news_reader.xml | 6 + .../src/main/res/values/colors.xml | 2 + .../src/main/res/values/strings.xml | 2 + 14 files changed, 601 insertions(+), 30 deletions(-) create mode 100644 News-Android-App/src/androidTest/java/de/luhmer/owncloudnewsreader/tests/DownloadWebPageServiceTest.java create mode 100644 News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/helper/NotificationActionReceiver.java create mode 100644 News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/services/DownloadWebPageService.java create mode 100644 News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/services/events/StopWebArchiveDownloadEvent.java diff --git a/News-Android-App/src/androidTest/java/de/luhmer/owncloudnewsreader/tests/DownloadWebPageServiceTest.java b/News-Android-App/src/androidTest/java/de/luhmer/owncloudnewsreader/tests/DownloadWebPageServiceTest.java new file mode 100644 index 00000000..ee51589f --- /dev/null +++ b/News-Android-App/src/androidTest/java/de/luhmer/owncloudnewsreader/tests/DownloadWebPageServiceTest.java @@ -0,0 +1,92 @@ +package de.luhmer.owncloudnewsreader.tests; + +import android.app.Activity; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.LargeTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.support.test.uiautomator.UiDevice; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.runner.RunWith; + +import de.luhmer.owncloudnewsreader.NewsReaderListActivity; +import de.luhmer.owncloudnewsreader.R; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class DownloadWebPageServiceTest { + + @Rule + public ActivityTestRule mActivityRule = new ActivityTestRule<>(NewsReaderListActivity.class); + + private UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + + private Activity getActivity() { + return mActivityRule.getActivity(); + } + + private String expectedAppName; + + + @Before + private void setUp() { + expectedAppName = getActivity().getString(R.string.app_name); + } + + /* + @Test + public void testStartDownload() { + openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getTargetContext()); + onView(withText(getActivity().getString(R.string.action_download_articles_offline))).perform(click()); + + } + + private void clearAllNotifications() { + UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + uiDevice.openNotification(); + long timeoutInMillis = 1000; + uiDevice.wait(Until.hasObject(By.textStartsWith(expectedAppName)), timeoutInMillis); + //UiObject2 clearAll = uiDevice.findObject(By.res(clearAllNotificationRes)); + //clearAll.click(); + } + + @Test + void shouldSendNotificationWhichContainsTitleTextAndAllCities() { + String expectedAppName = "Test"; + String expectedAllCities = "Test"; + String expectedTitle = "Test"; + String expectedText = "Test"; + // TODO do something here..! + uiDevice.openNotification(); + //uiDevice.wait(Until.hasObject(By.textStartsWith(expectedAppName)), timeout); + UiObject2 title = uiDevice.findObject(By.text(expectedTitle)); + UiObject2 text= uiDevice.findObject(By.textStartsWith(expectedText)); + //UiObject2 allCities= uiDevice.findObject(By.res(expectedAllCitiesActionRes)); + assertEquals(expectedTitle, title.getText()); + assertTrue(text.getText().startsWith(expectedText)); + //assertEquals(expectedAllCities.toLowerCase(), allCities.getText().toLowerCase()); + clearAllNotifications(); + } + + private class ClickOnSendNotification implements ViewAction { + + private final String TAG = ClickOnSendNotification.class.getCanonicalName(); + + public String getDescription() { + return "Click on the send notification button"; + } + + public Matcher getConstraints() { + return Matchers.allOf(isDisplayed(), isAssignableFrom(Button.class)); + } + + public void perform(@Nullable UiController uiController, @Nullable View view) { + //view.findViewById(R.id.stop).performClick(); + Log.d(TAG, "perform() called with: uiController = [" + uiController + "], view = [" + view + "]"); + } + } + + */ +} diff --git a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/Constants.java b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/Constants.java index 2257f923..a89e33f4 100644 --- a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/Constants.java +++ b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/Constants.java @@ -13,14 +13,12 @@ public class Constants { //public static final String LAST_SYNC = "LAST_SYNC"; public static final int maxItemsCount = 1500; + public static final String LAST_UPDATE_NEW_ITEMS_COUNT_STRING = "LAST_UPDATE_NEW_ITEMS_COUNT_STRING"; + public static final String NEWS_WEB_VERSION_NUMBER_STRING = "NewsWebVersionNumber"; + public static final String NOTIFICATION_ACTION_STOP_STRING = "NOTIFICATION_STOP"; public static final int MIN_NEXTCLOUD_FILES_APP_VERSION_CODE = 30030052; - public static final String LAST_UPDATE_NEW_ITEMS_COUNT_STRING = "LAST_UPDATE_NEW_ITEMS_COUNT_STRING"; - - - public static final String NEWS_WEB_VERSION_NUMBER_STRING = "NewsWebVersionNumber"; - public static boolean IsNextCloud(Context context) { SharedPreferences mPrefs = PreferenceManager.getDefaultSharedPreferences(context); diff --git a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/NewsDetailFragment.java b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/NewsDetailFragment.java index 45a6c5c9..1ead016a 100644 --- a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/NewsDetailFragment.java +++ b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/NewsDetailFragment.java @@ -46,11 +46,13 @@ import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.ProgressBar; +import android.widget.TextView; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.select.Elements; +import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; @@ -65,6 +67,7 @@ import de.luhmer.owncloudnewsreader.helper.AdBlocker; import de.luhmer.owncloudnewsreader.helper.AsyncTaskHelper; import de.luhmer.owncloudnewsreader.helper.ColorHelper; import de.luhmer.owncloudnewsreader.helper.ThemeChooser; +import de.luhmer.owncloudnewsreader.services.DownloadWebPageService; public class NewsDetailFragment extends Fragment implements RssItemToHtmlTask.Listener { @@ -78,6 +81,7 @@ public class NewsDetailFragment extends Fragment implements RssItemToHtmlTask.Li @BindView(R.id.webview) WebView mWebView; @BindView(R.id.progressBarLoading) ProgressBar mProgressBarLoading; @BindView(R.id.progressbar_webview) ProgressBar mProgressbarWebView; + @BindView(R.id.tv_offline_version) TextView mTvOfflineVersion; private int section_number; @@ -265,27 +269,32 @@ public class NewsDetailFragment extends Fragment implements RssItemToHtmlTask.Li SharedPreferences mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); int selectedBrowser = Integer.parseInt(mPrefs.getString(SettingsActivity.SP_DISPLAY_BROWSER, "0")); - boolean result = true; - switch(selectedBrowser) { - case 0: // Custom Tabs - CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); - builder.setToolbarColor(ContextCompat.getColor(getActivity(), R.color.colorPrimary)); - builder.setShowTitle(true); - builder.setStartAnimations(getActivity(), R.anim.slide_in_right, R.anim.slide_out_left); - builder.setExitAnimations(getActivity(), R.anim.slide_in_left, R.anim.slide_out_right); - builder.build().launchUrl(getActivity(), Uri.parse(url)); - result = true; - break; - case 1: // External Browser - Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivity(browserIntent); - break; - case 2: // Built in - result = super.shouldOverrideUrlLoading(view, url); - break; + File webArchiveFile = DownloadWebPageService.getWebPageArchiveFileForUrl(getActivity(), url); + if(webArchiveFile.exists()) { // Test if WebArchive exists for url + mTvOfflineVersion.setVisibility(View.VISIBLE); + mWebView.loadUrl("file://" + webArchiveFile.getAbsolutePath()); + return true; + } else { + mTvOfflineVersion.setVisibility(View.GONE); + switch (selectedBrowser) { + case 0: // Custom Tabs + CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder(); + builder.setToolbarColor(ContextCompat.getColor(getActivity(), R.color.colorPrimary)); + builder.setShowTitle(true); + builder.setStartAnimations(getActivity(), R.anim.slide_in_right, R.anim.slide_out_left); + builder.setExitAnimations(getActivity(), R.anim.slide_in_left, R.anim.slide_out_right); + builder.build().launchUrl(getActivity(), Uri.parse(url)); + return true; + case 1: // External Browser + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivity(browserIntent); + return true; + case 2: // Built in + return super.shouldOverrideUrlLoading(view, url); + default: + throw new IllegalStateException("Unknown selection!"); + } } - return result; - //return super.shouldOverrideUrlLoading(view, url); } @Override diff --git a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/NewsReaderListActivity.java b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/NewsReaderListActivity.java index 81c955b8..4f6bbec6 100644 --- a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/NewsReaderListActivity.java +++ b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/NewsReaderListActivity.java @@ -21,6 +21,7 @@ package de.luhmer.owncloudnewsreader; +import android.Manifest; import android.accounts.Account; import android.accounts.AccountManager; import android.app.Activity; @@ -31,9 +32,11 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Color; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.preference.PreferenceManager; @@ -91,6 +94,7 @@ import de.luhmer.owncloudnewsreader.helper.PostDelayHandler; import de.luhmer.owncloudnewsreader.helper.ThemeChooser; import de.luhmer.owncloudnewsreader.reader.nextcloud.RssItemObservable; import de.luhmer.owncloudnewsreader.services.DownloadImagesService; +import de.luhmer.owncloudnewsreader.services.DownloadWebPageService; import de.luhmer.owncloudnewsreader.services.OwnCloudSyncService; import de.luhmer.owncloudnewsreader.services.events.SyncFailedEvent; import de.luhmer.owncloudnewsreader.services.events.SyncFinishedEvent; @@ -145,6 +149,7 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements private SearchView searchView; private PublishSubject searchPublishSubject; + private static final int REQUEST_CODE_PERMISSION_DOWNLOAD_WEB_ARCHIVE = 1; @Override protected void onCreate(Bundle savedInstanceState) { @@ -780,7 +785,7 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements break; case R.id.menu_StartImageCaching: - DatabaseConnectionOrm dbConn = new DatabaseConnectionOrm(this); + final DatabaseConnectionOrm dbConn = new DatabaseConnectionOrm(this); long highestItemId = dbConn.getLowestRssItemIdUnread(); @@ -825,10 +830,36 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements searchView.setIconified(false); searchView.setFocusable(true); searchView.requestFocusFromTouch(); + return true; + + case R.id.menu_download_web_archive: + startDownloadWebPagesForOfflineReading(); + return true; } return super.onOptionsItemSelected(item); } + private void startDownloadWebPagesForOfflineReading() { + if (Build.VERSION.SDK_INT >= 23) { + if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + Log.v("Permission error","You have permission"); + } else { + Log.e("Permission error","Asking for permission"); + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_PERMISSION_DOWNLOAD_WEB_ARCHIVE); + return; + } + } + else { //you dont need to worry about these stuff below api level 23 + Log.v("Permission error","You already have the permission"); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(new Intent(this, DownloadWebPageService.class)); + } else { + startService(new Intent(this, DownloadWebPageService.class)); + } + } + private void DownloadMoreItems() { String username = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getString("edt_username", null); @@ -920,6 +951,19 @@ public class NewsReaderListActivity extends PodcastFragmentActivity implements } } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if(requestCode == REQUEST_CODE_PERMISSION_DOWNLOAD_WEB_ARCHIVE) { + startDownloadWebPagesForOfflineReading(); + } else { + Log.d(TAG, "No action defined here yet.."); + } + } + } + private void ensureCorrectTheme(Intent data) { SharedPreferences mPrefs = PreferenceManager.getDefaultSharedPreferences(this); String oldListLayout = data.getStringExtra(SettingsActivity.SP_FEED_LIST_LAYOUT); diff --git a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/SettingsActivity.java b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/SettingsActivity.java index 0c9016bd..bccf3b3a 100644 --- a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/SettingsActivity.java +++ b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/SettingsActivity.java @@ -67,6 +67,7 @@ import de.luhmer.owncloudnewsreader.helper.AppCompatPreferenceActivity; import de.luhmer.owncloudnewsreader.helper.ImageHandler; import de.luhmer.owncloudnewsreader.helper.PostDelayHandler; import de.luhmer.owncloudnewsreader.helper.ThemeChooser; +import de.luhmer.owncloudnewsreader.services.DownloadWebPageService; /** * A {@link PreferenceActivity} that presents a set of application settings. On @@ -648,9 +649,10 @@ public class SettingsActivity extends AppCompatPreferenceActivity { protected Void doInBackground(Void... params) { DatabaseConnectionOrm dbConn = new DatabaseConnectionOrm(context); dbConn.resetDatabase(); - ImageHandler.clearCache(); - return null; - } + ImageHandler.clearCache(); + DownloadWebPageService.clearWebArchiveCache(context); + return null; + } @Override protected void onPostExecute(Void result) { diff --git a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/database/DatabaseConnectionOrm.java b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/database/DatabaseConnectionOrm.java index a9877255..b6b0fe6b 100644 --- a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/database/DatabaseConnectionOrm.java +++ b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/database/DatabaseConnectionOrm.java @@ -343,6 +343,10 @@ public class DatabaseConnectionOrm { return daoSession.getRssItemDao().queryBuilder().where(RssItemDao.Properties.Read_temp.eq(false)).limit(100).orderDesc(RssItemDao.Properties.PubDate).listLazy(); } + public LazyList getAllUnreadRssItemsForDownloadWebPageService() { + return daoSession.getRssItemDao().queryBuilder().where(RssItemDao.Properties.Read_temp.eq(false)).orderDesc(RssItemDao.Properties.PubDate).listLazy(); + } + public LazyList getAllItemsWithIdHigher(long id) { return daoSession.getRssItemDao().queryBuilder().where(RssItemDao.Properties.Id.ge(id)).listLazy(); } diff --git a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/helper/NotificationActionReceiver.java b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/helper/NotificationActionReceiver.java new file mode 100644 index 00000000..237a65e1 --- /dev/null +++ b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/helper/NotificationActionReceiver.java @@ -0,0 +1,22 @@ +package de.luhmer.owncloudnewsreader.helper; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.greenrobot.eventbus.EventBus; + +import de.luhmer.owncloudnewsreader.services.events.StopWebArchiveDownloadEvent; + +import static de.luhmer.owncloudnewsreader.Constants.NOTIFICATION_ACTION_STOP_STRING; + +public class NotificationActionReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (NOTIFICATION_ACTION_STOP_STRING.equals(action)) { + EventBus.getDefault().post(new StopWebArchiveDownloadEvent()); + } + } +} diff --git a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/notification/NextcloudNotificationManager.java b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/notification/NextcloudNotificationManager.java index 8a9d3f39..15c1a5e0 100644 --- a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/notification/NextcloudNotificationManager.java +++ b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/notification/NextcloudNotificationManager.java @@ -77,6 +77,23 @@ public class NextcloudNotificationManager { return mNotificationDownloadImages; } + public static NotificationCompat.Builder BuildNotificationDownloadWebPageService(Context context, String channelId) { + getNotificationManagerAndCreateChannel(context, channelId); + + Intent intentNewsReader = new Intent(context, NewsReaderListActivity.class); + PendingIntent pIntent = PendingIntent.getActivity(context, 0, intentNewsReader, 0); + NotificationCompat.Builder mNotificationWebPages = new NotificationCompat.Builder(context, channelId) + .setContentTitle(context.getResources().getString(R.string.app_name)) + .setContentText("Downloading webpages for offline usage") + .setSmallIcon(R.drawable.ic_notification) + .setContentIntent(pIntent) + .setAutoCancel(true) + .setOnlyAlertOnce(true) + .setOngoing(true); + + return mNotificationWebPages; + } + public static void ShowNotificationImageDownloadLimitReached(Context context, String channelId, int limit) { diff --git a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/services/DownloadWebPageService.java b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/services/DownloadWebPageService.java new file mode 100644 index 00000000..0295ef7e --- /dev/null +++ b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/services/DownloadWebPageService.java @@ -0,0 +1,357 @@ +package de.luhmer.owncloudnewsreader.services; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import android.webkit.ConsoleMessage; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; + +import java.io.File; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import de.luhmer.owncloudnewsreader.R; +import de.luhmer.owncloudnewsreader.database.DatabaseConnectionOrm; +import de.luhmer.owncloudnewsreader.database.model.RssItem; +import de.luhmer.owncloudnewsreader.helper.NotificationActionReceiver; +import de.luhmer.owncloudnewsreader.notification.NextcloudNotificationManager; +import de.luhmer.owncloudnewsreader.services.events.StopWebArchiveDownloadEvent; + +import static de.luhmer.owncloudnewsreader.Constants.NOTIFICATION_ACTION_STOP_STRING; + +/** + * An {@link Service} subclass for handling asynchronous task requests in + * a service on a separate handler thread. + *

+ * helper methods. + */ +public class DownloadWebPageService extends Service { + + private static final String TAG = DownloadWebPageService.class.getCanonicalName(); + private static final int JOB_ID = 1002; + + static final String CHANNEL_ID = "Download Web Page Service"; + private static final String WebArchiveFinalPrefix = "web_archive_"; + private static final int NUMBER_OF_CORES = 4; + private NotificationCompat.Builder mNotificationWebPages; + private static final int NOTIFICATION_ID = JOB_ID; + private NotificationManager mNotificationManager; + + + // Sets the amount of time an idle thread waits before terminating + private static final int KEEP_ALIVE_TIME = 1; + // Sets the Time Unit to seconds + private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS; + + private final AtomicInteger doneCount = new AtomicInteger(); + private Integer totalCount = 0; + + private ThreadPoolExecutor mDownloadThreadPool; + + + + @Override + public void onCreate() { + Log.d(TAG, "onCreate() called"); + super.onCreate(); + + initNotification(); + + downloadWebPages(); + + EventBus.getDefault().register(this); + } + + @Override + public void onDestroy() { + Log.d(TAG, "onDestroy() called"); + mNotificationManager.cancel(NOTIFICATION_ID); + EventBus.getDefault().unregister(this); + super.onDestroy(); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private void initNotification() { + mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + mNotificationWebPages = NextcloudNotificationManager.BuildNotificationDownloadWebPageService(this, CHANNEL_ID); + + Intent stopIntent = new Intent(this, NotificationActionReceiver.class); + stopIntent.setAction(NOTIFICATION_ACTION_STOP_STRING); + PendingIntent stopPendingIntent = PendingIntent.getBroadcast(this, 0, stopIntent, PendingIntent.FLAG_ONE_SHOT); + mNotificationWebPages.addAction(R.drawable.ic_action_pause, "Stop", stopPendingIntent); + } + + @Subscribe + public void onEvent(StopWebArchiveDownloadEvent event) { + mDownloadThreadPool.shutdownNow(); + stopSelf(); + } + + private void runOnMainThreadAndWait(final Runnable runnable) throws InterruptedException { + synchronized(runnable) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + @Override + public void run() { + runnable.run(); + synchronized (runnable) { + runnable.notifyAll(); + } + } + }); + runnable.wait(); // unlocks runnable while waiting + } + } + + private void delayedRunOnMainThread(Runnable runnable, int waitMillis) { + try { + Thread.sleep(waitMillis); + runOnMainThreadAndWait(runnable); + } catch (InterruptedException e) { + Log.e(TAG, "Error occurred..", e); + } + } + + + + private void downloadWebPages() { + mNotificationWebPages.setProgress(0, 100, true); + mNotificationManager.notify(NOTIFICATION_ID, mNotificationWebPages.build()); + + final DatabaseConnectionOrm dbConn = new DatabaseConnectionOrm(DownloadWebPageService.this); + final BlockingQueue downloadWorkQueue = new LinkedBlockingQueue<>(); + + getWebPageArchiveStorage(this).mkdirs(); + + for (RssItem rssItem : dbConn.getAllUnreadRssItemsForDownloadWebPageService()) { + downloadWorkQueue.add(new DownloadWebPage(rssItem.getLink())); + } + //downloadWorkQueue.clear(); + + /* + List items = dbConn.getAllUnreadRssItemsForDownloadWebPageService(); + for (int i = 0; i < 5; i++) { + downloadWorkQueue.add(new DownloadWebPage(items.get(i).getLink())); + } + */ + + startDownloadingQueue(downloadWorkQueue); + } + + public static void clearWebArchiveCache(Context context) { + getWebPageArchiveStorage(context).mkdirs(); + + String path = getWebPageArchiveStorage(context).getAbsolutePath(); + Log.d("Files", "Path: " + path); + File directory = new File(path); + File[] files = directory.listFiles(); + Log.d("Files", "Size: " + files.length); + for (File file : files) { + String name = file.getName(); + //og.d("Files", "FileName: " + file.getName()); + if (name.startsWith(WebArchiveFinalPrefix)) { + Log.v(TAG, "Deleting file: " + name); + //file.delete(); + } + } + } + + private void startDownloadingQueue(BlockingQueue downloadWorkQueue) { + totalCount = downloadWorkQueue.size(); + + // Creates a thread pool manager + mDownloadThreadPool = new ThreadPoolExecutor( + NUMBER_OF_CORES, // Initial pool size + NUMBER_OF_CORES, // Max pool size + KEEP_ALIVE_TIME, + KEEP_ALIVE_TIME_UNIT, + downloadWorkQueue); + + // Start all tasks in queue + mDownloadThreadPool.prestartAllCoreThreads(); + + // Tell ThreadPoolExecutor to stop once done + mDownloadThreadPool.shutdown(); + + // If no articles are present, remove notification right away. Otherwise the user has to close it manually + if(totalCount == 0) { + mNotificationManager.cancel(NOTIFICATION_ID); + } + } + + class DownloadWebPage implements Runnable { + + private String url; + private WebView webView; + final Object lock; + + DownloadWebPage(String url) { + this.url = url; + lock = new Object(); + } + + @Override + public void run() { + //Log.v(TAG, "Running DownloadWebPage for url: " + url); + synchronized (lock) { + File webArchiveFile = getWebPageArchiveFileForUrl(DownloadWebPageService.this, url); + if (!webArchiveFile.exists()) { + //Log.v(TAG, "Loading page:"); + initWebView(); + loadUrlInWebViewAndWait(); + } /* else { + Log.v(TAG, "Already cached article: " + url); + } */ + } + updateNotificationProgress(); + } + + private void initWebView() { + try { + runOnMainThreadAndWait(new Runnable() { + @Override + public void run() { + webView = new WebView(DownloadWebPageService.this); + webView.setWebViewClient(new DownloadImageWebViewClient(lock)); + webView.setWebChromeClient(new DownloadImageWebViewChromeClient()); + } + }); + } catch (InterruptedException e) { + Log.e(TAG, "Error while setting up WebView", e); + } + } + + private void loadUrlInWebViewAndWait() { + try { + runOnMainThreadAndWait(new Runnable() { + @Override + public void run() { + webView.loadUrl(url); + } + }); + lock.wait(); + } catch (InterruptedException e) { + Log.e(TAG, "Error while opening url", e); + } + } + } + + class DownloadImageWebViewChromeClient extends WebChromeClient { + @Override + public boolean onConsoleMessage(ConsoleMessage cm) { + //Log.d("TAG", cm.message() + " at " + cm.sourceId() + ":" + cm.lineNumber()); + return true; + } + } + + class DownloadImageWebViewClient extends WebViewClient { + private final String TAG = DownloadImageWebViewClient.class.getName(); + private final Object lock; + boolean failed = false; + + DownloadImageWebViewClient(Object lock) { + this.lock = lock; + } + + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + //Log.e(TAG, "onReceivedError() called with: view = [" + view + "], request = [" + request + "], error = [" + error + "]"); + failed = true; + super.onReceivedError(view, request, error); + } + + public void onPageFinished(final WebView view, final String url) { + //Log.e(TAG, "onPageFinished() called with: view = [" + view + "], url = [" + url + "]"); + + if(failed) { + Log.e(TAG, "Skipping onPageFinished as request failed.. " + url); + } else { + saveWebArchive(view, url); + } + + // Notify waiting thread that we're done.. + synchronized (lock) { + lock.notifyAll(); + } + } + + private void saveWebArchive(final WebView view, final String url) { + new Thread(new Runnable() { + @Override + public void run() { + delayedRunOnMainThread(new Runnable() { + @Override + public void run() { + // Can't store directly on external dir.. (workaround -> store on internal storage first and move then)) + final File webArchive = getWebPageArchiveFileForUrl(DownloadWebPageService.this, url); + final File webArchiveExternalStorage = getWebPageArchiveFileForUrl(DownloadWebPageService.this, url); + view.saveWebArchive(webArchive.getAbsolutePath(), false, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + // Move file to external storage once done writing + webArchive.renameTo(webArchiveExternalStorage); + //boolean success = webArchive.renameTo(webArchiveExternalStorage); + //Log.v(TAG, "Move succeeded: " + success); + } + }); + } + }, 2000); + } + }).start(); + } + } + + private synchronized void updateNotificationProgress() { + int current = doneCount.incrementAndGet(); + Log.d(TAG, String.format("updateNotificationProgress (%d/%d)", current, totalCount)); + + if(current == totalCount) { + //mNotificationManager.cancel(NOTIFICATION_ID); + EventBus.getDefault().post(new StopWebArchiveDownloadEvent()); + } else { + mNotificationWebPages + .setContentText((current) + "/" + totalCount + " - Downloading Images for offline usage") + .setProgress(totalCount, current, false); + + mNotificationManager.notify(NOTIFICATION_ID, mNotificationWebPages.build()); + } + } + + public static File getWebPageArchiveStorage(Context context) { + //return context.getFilesDir(); + //return new File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "nextcloud-news/web-archive/"); + + return new File(context.getExternalCacheDir(), "web-archive/"); + //return new File(Environment.getExternalStorageDirectory(), "nextcloud-news/web-archive/"); + } + + public static File getWebPageArchiveFileForUrl(Context context, String url) { + return new File(getWebPageArchiveStorage(context), getWebPageArchiveFilename(url)); + } + + public static String getWebPageArchiveFilename(String url) { + return WebArchiveFinalPrefix + url.hashCode() + ".mht"; + } +} diff --git a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/services/events/StopWebArchiveDownloadEvent.java b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/services/events/StopWebArchiveDownloadEvent.java new file mode 100644 index 00000000..05d22fe6 --- /dev/null +++ b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/services/events/StopWebArchiveDownloadEvent.java @@ -0,0 +1,4 @@ +package de.luhmer.owncloudnewsreader.services.events; + +public class StopWebArchiveDownloadEvent { +} diff --git a/News-Android-App/src/main/res/layout/fragment_news_detail.xml b/News-Android-App/src/main/res/layout/fragment_news_detail.xml index 537bab8a..d44fa22f 100644 --- a/News-Android-App/src/main/res/layout/fragment_news_detail.xml +++ b/News-Android-App/src/main/res/layout/fragment_news_detail.xml @@ -10,7 +10,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignParentTop="true" - android:hardwareAccelerated="true" /> + android:hardwareAccelerated="true" + android:layout_above="@id/tv_offline_version"/> + + \ No newline at end of file diff --git a/News-Android-App/src/main/res/menu/news_reader.xml b/News-Android-App/src/main/res/menu/news_reader.xml index 2fe1d412..5a1ba707 100644 --- a/News-Android-App/src/main/res/menu/news_reader.xml +++ b/News-Android-App/src/main/res/menu/news_reader.xml @@ -17,6 +17,12 @@ app:actionViewClass="android.widget.SearchView" app:showAsAction="always|collapseActionView" /> + + #757575 #212121 + #e53935 + #eeeeee diff --git a/News-Android-App/src/main/res/values/strings.xml b/News-Android-App/src/main/res/values/strings.xml index c9adbb30..54ce127f 100644 --- a/News-Android-App/src/main/res/values/strings.xml +++ b/News-Android-App/src/main/res/values/strings.xml @@ -27,6 +27,7 @@ Download more items Thumbnail + Showing cached version Starred @@ -40,6 +41,7 @@ Sync Settings Add new feed Read out + Download articles offline You have %d new unread item You have %d new unread items From 1793ea7b11cc8ae06b6477e3ff661efb8f927447 Mon Sep 17 00:00:00 2001 From: David Luhmer Date: Sun, 28 Oct 2018 13:02:33 +0100 Subject: [PATCH 2/3] Fix codacy issues --- News-Android-App/src/main/AndroidManifest.xml | 115 ++++++++---------- .../NewsDetailFragment.java | 8 +- .../NextcloudNotificationManager.java | 2 +- .../services/DownloadWebPageService.java | 8 +- 4 files changed, 62 insertions(+), 71 deletions(-) diff --git a/News-Android-App/src/main/AndroidManifest.xml b/News-Android-App/src/main/AndroidManifest.xml index a3b45666..de5a93f3 100644 --- a/News-Android-App/src/main/AndroidManifest.xml +++ b/News-Android-App/src/main/AndroidManifest.xml @@ -2,10 +2,9 @@ - + android:versionName="0.9.9.19"> @@ -20,25 +19,21 @@ - - - + @@ -48,37 +43,29 @@ - - - + android:label="@string/title_activity_news_detail"> - - - - - + android:label="@string/title_activity_settings"> + - - + android:label="@string/title_activity_sync_interval_selector"> - + android:windowSoftInputMode="adjustResize|stateVisible"> + + @@ -86,25 +73,32 @@ - - - - - + + + + - + + - - + @@ -113,7 +107,6 @@ - + + + + + + + - + @@ -197,26 +197,17 @@ android:name=".widget.WidgetService" android:exported="false" android:permission="android.permission.BIND_REMOTEVIEWS" /> - - - + android:exported="true"> - - - - - + android:exported="false"> - - + \ No newline at end of file diff --git a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/NewsDetailFragment.java b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/NewsDetailFragment.java index 1ead016a..146ddc72 100644 --- a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/NewsDetailFragment.java +++ b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/NewsDetailFragment.java @@ -78,10 +78,10 @@ public class NewsDetailFragment extends Fragment implements RssItemToHtmlTask.Li public static int background_color = Integer.MIN_VALUE; - @BindView(R.id.webview) WebView mWebView; - @BindView(R.id.progressBarLoading) ProgressBar mProgressBarLoading; - @BindView(R.id.progressbar_webview) ProgressBar mProgressbarWebView; - @BindView(R.id.tv_offline_version) TextView mTvOfflineVersion; + protected @BindView(R.id.webview) WebView mWebView; + protected @BindView(R.id.progressBarLoading) ProgressBar mProgressBarLoading; + protected @BindView(R.id.progressbar_webview) ProgressBar mProgressbarWebView; + protected @BindView(R.id.tv_offline_version) TextView mTvOfflineVersion; private int section_number; diff --git a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/notification/NextcloudNotificationManager.java b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/notification/NextcloudNotificationManager.java index 15c1a5e0..7762387d 100644 --- a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/notification/NextcloudNotificationManager.java +++ b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/notification/NextcloudNotificationManager.java @@ -77,7 +77,7 @@ public class NextcloudNotificationManager { return mNotificationDownloadImages; } - public static NotificationCompat.Builder BuildNotificationDownloadWebPageService(Context context, String channelId) { + public static NotificationCompat.Builder buildNotificationDownloadWebPageService(Context context, String channelId) { getNotificationManagerAndCreateChannel(context, channelId); Intent intentNewsReader = new Intent(context, NewsReaderListActivity.class); diff --git a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/services/DownloadWebPageService.java b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/services/DownloadWebPageService.java index 0295ef7e..57e85cb8 100644 --- a/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/services/DownloadWebPageService.java +++ b/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/services/DownloadWebPageService.java @@ -48,7 +48,7 @@ public class DownloadWebPageService extends Service { private static final String TAG = DownloadWebPageService.class.getCanonicalName(); private static final int JOB_ID = 1002; - static final String CHANNEL_ID = "Download Web Page Service"; + private static final String CHANNEL_ID = "Download Web Page Service"; private static final String WebArchiveFinalPrefix = "web_archive_"; private static final int NUMBER_OF_CORES = 4; private NotificationCompat.Builder mNotificationWebPages; @@ -95,7 +95,7 @@ public class DownloadWebPageService extends Service { private void initNotification() { mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - mNotificationWebPages = NextcloudNotificationManager.BuildNotificationDownloadWebPageService(this, CHANNEL_ID); + mNotificationWebPages = NextcloudNotificationManager.buildNotificationDownloadWebPageService(this, CHANNEL_ID); Intent stopIntent = new Intent(this, NotificationActionReceiver.class); stopIntent.setAction(NOTIFICATION_ACTION_STOP_STRING); @@ -205,7 +205,7 @@ public class DownloadWebPageService extends Service { private String url; private WebView webView; - final Object lock; + private final Object lock; DownloadWebPage(String url) { this.url = url; @@ -269,7 +269,7 @@ public class DownloadWebPageService extends Service { class DownloadImageWebViewClient extends WebViewClient { private final String TAG = DownloadImageWebViewClient.class.getName(); private final Object lock; - boolean failed = false; + private boolean failed = false; DownloadImageWebViewClient(Object lock) { this.lock = lock; From 516c84405c7dc5cd436e6d15f17e44aac653f3c7 Mon Sep 17 00:00:00 2001 From: David Luhmer Date: Sun, 28 Oct 2018 13:09:25 +0100 Subject: [PATCH 3/3] Fix codacy issues --- .../tests/DownloadWebPageServiceTest.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/News-Android-App/src/androidTest/java/de/luhmer/owncloudnewsreader/tests/DownloadWebPageServiceTest.java b/News-Android-App/src/androidTest/java/de/luhmer/owncloudnewsreader/tests/DownloadWebPageServiceTest.java index ee51589f..493c3ac0 100644 --- a/News-Android-App/src/androidTest/java/de/luhmer/owncloudnewsreader/tests/DownloadWebPageServiceTest.java +++ b/News-Android-App/src/androidTest/java/de/luhmer/owncloudnewsreader/tests/DownloadWebPageServiceTest.java @@ -1,41 +1,35 @@ package de.luhmer.owncloudnewsreader.tests; -import android.app.Activity; -import android.support.test.InstrumentationRegistry; import android.support.test.filters.LargeTest; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; -import android.support.test.uiautomator.UiDevice; -import org.junit.Before; import org.junit.Rule; import org.junit.runner.RunWith; import de.luhmer.owncloudnewsreader.NewsReaderListActivity; -import de.luhmer.owncloudnewsreader.R; @RunWith(AndroidJUnit4.class) @LargeTest public class DownloadWebPageServiceTest { + //private String expectedAppName; + @Rule public ActivityTestRule mActivityRule = new ActivityTestRule<>(NewsReaderListActivity.class); + /* private UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); private Activity getActivity() { return mActivityRule.getActivity(); } - private String expectedAppName; - - @Before private void setUp() { expectedAppName = getActivity().getString(R.string.app_name); } - /* @Test public void testStartDownload() { openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getTargetContext());