/** This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. **/ /** This file is part of Save For Offline, an Android app which saves / downloads complete webpages for offine reading. **/ /** If you modify, redistribute, or write something based on this or parts of it, you MUST, I repeat, you MUST comply with the GPLv2+ license. This means that if you use or modify my code, you MUST release the source code of your modified version, if / when this is required under the terms of the license. If you cannot / do not want to do this, DO NOT USE MY CODE. Thanks. (I've added this message to to the source because it's been used in severeral proprietary closed source apps, which I don't want, and which is also a violation of the liense.) **/ /** Written by Jonas Czech (JonasCz, stackoverflow.com/users/4428462/JonasCz and github.com/JonasCz). (4428462jonascz/eafc4d1afq) **/ package jonas.tool.saveForOffline; import android.app.Service; import android.content.Intent; import android.content.SharedPreferences; import android.os.IBinder; import android.preference.PreferenceManager; import android.util.Log; import android.widget.Toast; import java.io.File; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class SaveService extends Service { private final String TAG = "SaveService"; private ThreadPoolExecutor executor; private SharedPreferences sharedPreferences; private PageSaver pageSaver; private NotificationTools notificationTools; @Override public void onCreate() { executor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(SaveService.this); pageSaver = new PageSaver(new PageSaveEventCallback()); notificationTools = new NotificationTools(this); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (intent.getBooleanExtra("USER_CANCELLED", false) || intent.getBooleanExtra("USER_CANCELLED_ALL", false)) { if (intent.getBooleanExtra("USER_CANCELLED_ALL", false)) { executor.getQueue().clear(); } //cancelling okhttp seems to cause networkOnMainThreadException, hence this. Log.w(TAG, "Cancelled"); new Thread(new Runnable() { @Override public void run() { pageSaver.cancel(); } }).start(); return START_NOT_STICKY; } String pageUrl = intent.getStringExtra(Intent.EXTRA_TEXT); if (pageUrl != null && pageUrl.startsWith("http")) { executor.submit(new PageSaveTask(pageUrl)); } else { if (pageUrl == null) { notificationTools.notifyFailure("URL null, this is probably a bug", null); } else { notificationTools.notifyFailure("URL not valid: " + pageUrl, null); } } return START_NOT_STICKY; } private class PageSaveTask implements Runnable { private final String pageUrl; private String destinationDirectory; public PageSaveTask(String pageUrl) { this.pageUrl = pageUrl; this.destinationDirectory = DirectoryHelper.getDestinationDirectory(sharedPreferences); } @Override public void run() { try { pageSaver.resetState(); notificationTools.notifySaveStarted(executor.getQueue().size()); pageSaver.getOptions().setUserAgent(sharedPreferences.getString("user_agent", getResources().getStringArray(R.array.entries_list_preference)[1])); //cache is leaking and growing forever for some reason, so disable cache for now. //pageSaver.getOptions().setCache(getApplicationContext().getExternalCacheDir(), 1024 * 1024 * 15); boolean success = pageSaver.getPage(pageUrl, destinationDirectory, "index.html"); if (pageSaver.isCancelled() || !success) { DirectoryHelper.deleteDirectory(new File(destinationDirectory)); if (pageSaver.isCancelled()) { //user cancelled, remove the notification, and delete files. Log.e("SaveService", "Stopping Service, (Cancelled). Deleting files in: " + destinationDirectory + ", from: " + pageUrl); notificationTools.cancelAll(); stopService(); } else if (!success) { //something went wrong, leave the notification, and delete files. Log.e("SaveService", "Failed. Deleting files in: " + destinationDirectory + ", from: " + pageUrl); } return; } notificationTools.updateText(null, "Finishing...", executor.getQueue().size()); File oldSavedPageDirectory = new File(destinationDirectory); File newSavedPageDirectory = new File(getNewDirectoryPath(pageSaver.getPageTitle(), oldSavedPageDirectory.getPath())); oldSavedPageDirectory.renameTo(newSavedPageDirectory); new Database(SaveService.this).addToDatabase(newSavedPageDirectory.getPath() + File.separator, pageSaver.getPageTitle(), pageUrl); if (sharedPreferences.getBoolean("generate_saved_page_thumbnails", true)) { Intent i = new Intent(SaveService.this, ScreenshotService.class); i.putExtra(Database.FILE_LOCATION, "file://" + newSavedPageDirectory.getPath() + File.separator + "index.html"); i.putExtra(Database.ORIGINAL_URL, pageUrl); i.putExtra(Database.THUMBNAIL, newSavedPageDirectory + File.separator + "saveForOffline_thumbnail.png"); startService(i); } stopService(); notificationTools.notifyFinished(pageSaver.getPageTitle(), newSavedPageDirectory.getPath()); } catch (Exception e) { //so that exceptions don't fpget swallowed and we see them. Toast.makeText(SaveService.this, "SaveService Exception: " + e.getMessage(), Toast.LENGTH_LONG).show(); e.printStackTrace(); } } private String getNewDirectoryPath(String title, String oldDirectoryPath) { String returnString = title.replaceAll("[^a-zA-Z0-9-_\\.]", "_") + DirectoryHelper.createUniqueFilename(); //TODO: Fix this to support non A-Z & 0-9 characters File f = new File(oldDirectoryPath); return f.getParentFile().getAbsolutePath() + File.separator + returnString + File.separator; } } private class PageSaveEventCallback implements EventCallback { @Override public void onFatalError(final Throwable e, String pageUrl) { Log.e("PageSaverService", e.getMessage(), e); stopService(); notificationTools.notifyFailure(e.getMessage(), pageUrl); } @Override public void onProgressChanged(final int progress, final int maxProgress, final boolean indeterminate) { notificationTools.updateProgress(progress, maxProgress, indeterminate, executor.getQueue().size()); } @Override public void onProgressMessage(final String message) { notificationTools.updateText(null, message, executor.getQueue().size()); } @Override public void onPageTitleAvailable(String pageTitle) { notificationTools.updateText(pageTitle, null, executor.getQueue().size()); } @Override public void onLogMessage(final String message) { Log.d("PageSaverService", message); } @Override public void onError(final Throwable e) { Log.e("PageSaverService", e.getMessage(), e); } @Override public void onError(String errorMessage) { Log.e(TAG, errorMessage); } } private void stopService() { if (executor.getQueue().isEmpty()) { stopSelf(); } } @Override public void onDestroy() { Log.d(TAG, "Service destroyed"); } @Override public IBinder onBind(Intent i) { return null; } }