Merge pull request #675 from nextcloud/download-article

Add support to download full articles for offline viewing (using WebA…
This commit is contained in:
David Luhmer 2018-10-28 17:50:16 +01:00 committed by GitHub
commit 12eb597f80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 651 additions and 95 deletions

View file

@ -0,0 +1,86 @@
package de.luhmer.owncloudnewsreader.tests;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.runner.RunWith;
import de.luhmer.owncloudnewsreader.NewsReaderListActivity;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class DownloadWebPageServiceTest {
//private String expectedAppName;
@Rule
public ActivityTestRule<NewsReaderListActivity> mActivityRule = new ActivityTestRule<>(NewsReaderListActivity.class);
/*
private UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
private Activity getActivity() {
return mActivityRule.getActivity();
}
@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<View> 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 + "]");
}
}
*/
}

View file

@ -2,10 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="de.luhmer.owncloudnewsreader"
android:installLocation="internalOnly"
android:versionCode="134"
android:versionName="0.9.9.19"
android:installLocation="internalOnly">
android:versionName="0.9.9.19">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.INTERNET" />
@ -20,25 +19,21 @@
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<!--
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
-->
<!-- <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" /> -->
<application
android:name=".NewsReaderApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:name=".NewsReaderApplication"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
tools:replace="android:icon, android:label, android:theme, android:name">
<activity
android:name=".NewsReaderListActivity"
android:label="@string/app_name"
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name"
android:launchMode="singleTop">
<!-- android:configChanges="keyboardHidden|orientation|screenSize" -->
@ -48,37 +43,29 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".NewsDetailActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/title_activity_news_detail" >
</activity>
android:label="@string/title_activity_news_detail"></activity>
<activity
android:name=".SettingsActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/title_activity_settings" >
</activity>
<activity android:name=".DownloadImagesActivity">
</activity>
android:label="@string/title_activity_settings"></activity>
<activity android:name=".DownloadImagesActivity"></activity>
<activity
android:name=".SyncIntervalSelectorActivity"
android:label="@string/title_activity_sync_interval_selector" >
</activity>
android:label="@string/title_activity_sync_interval_selector"></activity>
<activity
android:name=".NewFeedActivity"
android:label="@string/title_activity_new_feed"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize|stateVisible" >
android:windowSoftInputMode="adjustResize|stateVisible">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
<data android:host="*" />
<data android:pathPattern=".*\\atom.xml" />
@ -86,25 +73,32 @@
<data android:pathPattern=".*\\.rss" />
<data android:pathPattern=".*/feed" />
<data android:pathPattern=".*feed/*" />
<data android:scheme="http" android:host="*"
android:pathPattern=".*\\.opml" />
<data android:scheme="https" android:host="*"
android:pathPattern=".*\\.opml" />
<data android:scheme="content" android:host="*"
android:pathPattern=".*\\.opml" />
<data android:scheme="file" android:host="*"
android:pathPattern=".*\\.opml" />
<data
android:host="*"
android:pathPattern=".*\\.opml"
android:scheme="http" />
<data
android:host="*"
android:pathPattern=".*\\.opml"
android:scheme="https" />
<data
android:host="*"
android:pathPattern=".*\\.opml"
android:scheme="content" />
<data
android:host="*"
android:pathPattern=".*\\.opml"
android:scheme="file" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
<activity android:name="DirectoryChooserActivity" />
<activity android:name=".DirectoryChooserActivity" />
<receiver android:name=".events.podcast.broadcastreceiver.PodcastNotificationToggle">
<intent-filter>
@ -113,7 +107,6 @@
</intent-filter>
</receiver>
<!--
**********************************************************************
* Sync Adapter and Service
@ -123,12 +116,15 @@
<service
android:name=".services.DownloadImagesService"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service android:name=".services.SyncItemStateService"
<service
android:name=".services.SyncItemStateService"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".services.DownloadWebPageService"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".services.OwnCloudAuthenticatorService"
android:exported="true" >
android:exported="true">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
@ -137,11 +133,10 @@
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service android:name=".services.OwnCloudSyncService" />
<service
android:name=".services.OwnCloudSettingsSyncService"
android:exported="true" >
android:exported="true">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
@ -155,9 +150,7 @@
android:name=".providers.OwnCloudSyncProvider"
android:authorities="de.luhmer.owncloudnewsreader"
android:label="@string/auto_sync_string"
android:syncable="true" >
</provider>
android:syncable="true"></provider>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
@ -177,13 +170,20 @@
<!-- android:theme="@style/Theme.Transparent" > -->
<!-- </activity> -->
<receiver android:name=".helper.NotificationActionReceiver">
<intent-filter>
<action android:name="YES_ACTION"/>
<action android:name="STOP_ACTION"/>
</intent-filter>
</receiver>
<!--
**********************************************************************
* Widget Provider Receiver
**********************************************************************
-->
<receiver android:name=".widget.WidgetProvider" >
<receiver android:name=".widget.WidgetProvider">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
@ -197,26 +197,17 @@
android:name=".widget.WidgetService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name=".services.PodcastPlaybackService"
android:enabled="true"
android:exported="true" >
</service>
android:exported="true"></service>
<service
android:name=".services.PodcastDownloadService"
android:exported="false" >
</service>
android:exported="false"></service>
<service
android:name="de.luhmer.owncloudnewsreader.chrometabs.KeepAliveService"
android:name=".chrometabs.KeepAliveService"
android:exported="true"
android:process=":remote" />
</application>
</manifest>
</manifest>

View file

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

View file

@ -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 {
@ -75,9 +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;
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;
@ -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

View file

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

View file

@ -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) {

View file

@ -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<RssItem> getAllUnreadRssItemsForDownloadWebPageService() {
return daoSession.getRssItemDao().queryBuilder().where(RssItemDao.Properties.Read_temp.eq(false)).orderDesc(RssItemDao.Properties.PubDate).listLazy();
}
public LazyList<RssItem> getAllItemsWithIdHigher(long id) {
return daoSession.getRssItemDao().queryBuilder().where(RssItemDao.Properties.Id.ge(id)).listLazy();
}

View file

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

View file

@ -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) {

View file

@ -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.
* <p>
* helper methods.
*/
public class DownloadWebPageService extends Service {
private static final String TAG = DownloadWebPageService.class.getCanonicalName();
private static final int JOB_ID = 1002;
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;
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<Runnable> downloadWorkQueue = new LinkedBlockingQueue<>();
getWebPageArchiveStorage(this).mkdirs();
for (RssItem rssItem : dbConn.getAllUnreadRssItemsForDownloadWebPageService()) {
downloadWorkQueue.add(new DownloadWebPage(rssItem.getLink()));
}
//downloadWorkQueue.clear();
/*
List<RssItem> 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<Runnable> 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;
private 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;
private 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<String>() {
@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";
}
}

View file

@ -0,0 +1,4 @@
package de.luhmer.owncloudnewsreader.services.events;
public class StopWebArchiveDownloadEvent {
}

View file

@ -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"/>
<ProgressBar
android:id="@+id/progressbar_webview"
@ -30,5 +31,16 @@
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
<TextView
android:id="@+id/tv_offline_version"
android:layout_width="match_parent"
android:layout_height="25dp"
android:layout_alignParentBottom="true"
android:text="@string/tv_showing_cached_version"
android:gravity="center"
android:textColor="@android:color/black"
android:background="@color/material_red_600"
android:visibility="gone" />
</RelativeLayout>

View file

@ -17,6 +17,12 @@
app:actionViewClass="android.widget.SearchView"
app:showAsAction="always|collapseActionView" />
<item
android:id="@+id/menu_download_web_archive"
android:enabled="true"
android:title="@string/action_download_articles_offline"
app:showAsAction="never" />
<item android:id="@+id/menu_StartImageCaching"
android:title="@string/menu_StartImageCaching"
android:orderInCategory="95"

View file

@ -19,6 +19,8 @@
<color name="material_grey_600" tools:override="true">#757575</color>
<color name="material_grey_900" tools:override="true">#212121</color>
<color name="material_red_600" tools:override="true">#e53935</color>
<!-- see also assets/web.css -->
<color name="news_detail_background_color">#eeeeee</color>

View file

@ -27,6 +27,7 @@
<string name="menu_downloadMoreItems">Download more items</string>
<string name="img_view_thumbnail" translatable="false">Thumbnail</string>
<string name="tv_showing_cached_version">Showing cached version</string>
<!-- Action Bar Items -->
<string name="action_starred">Starred</string>
@ -40,6 +41,7 @@
<string name="action_sync_settings">Sync Settings</string>
<string name="action_add_new_feed">Add new feed</string>
<string name="action_textToSpeech">Read out</string>
<string name="action_download_articles_offline">Download articles offline</string>
<plurals name="notification_new_items_ticker">
<item quantity="one">You have %d new unread item</item>
<item quantity="other">You have %d new unread items</item>