Skip to content
This repository has been archived by the owner on Dec 24, 2022. It is now read-only.

Add starred items export and reset features #260

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.2'
classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'com.droidtitan:lint-cleaner-plugin:+'

// NOTE: Do not place your application dependencies here; they belong
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
import net.etuldan.sparss.adapter.FeedsCursorAdapter;
import net.etuldan.sparss.parser.OPML;
import net.etuldan.sparss.provider.FeedData.FeedColumns;
import net.etuldan.sparss.utils.HTMLDigest;
import net.etuldan.sparss.view.DragNDropExpandableListView;
import net.etuldan.sparss.view.DragNDropListener;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,24 @@

package net.etuldan.sparss.fragment;

import android.Manifest;
import android.app.LoaderManager;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.CursorLoader;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.Loader;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.SearchView;
import android.view.GestureDetector;
Expand All @@ -53,13 +59,15 @@
import com.melnykov.fab.FloatingActionButton;

import net.etuldan.sparss.Constants;
import net.etuldan.sparss.MainApplication;
import net.etuldan.sparss.R;
import net.etuldan.sparss.activity.HomeActivity;
import net.etuldan.sparss.adapter.EntriesCursorAdapter;
import net.etuldan.sparss.provider.FeedData;
import net.etuldan.sparss.provider.FeedData.EntryColumns;
import net.etuldan.sparss.provider.FeedDataContentProvider;
import net.etuldan.sparss.service.FetcherService;
import net.etuldan.sparss.utils.HTMLDigest;
import net.etuldan.sparss.utils.PrefUtils;
import net.etuldan.sparss.utils.UiUtils;

Expand All @@ -77,6 +85,8 @@ public class EntriesListFragment extends SwipeRefreshListFragment {
private static final int ENTRIES_LOADER_ID = 1;
private static final int NEW_ENTRIES_NUMBER_LOADER_ID = 2;

private static final int PERMISSIONS_REQUEST_EXPORT_STARRED = 1;

private Uri mUri;
private boolean mShowFeedInfo = false;
private EntriesCursorAdapter mEntriesCursorAdapter;
Expand Down Expand Up @@ -310,6 +320,8 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
menu.findItem(R.id.menu_refresh).setVisible(false);
} else {
menu.findItem(R.id.menu_share_starred).setVisible(false);
menu.findItem(R.id.menu_export_starred).setVisible(false);
menu.findItem(R.id.menu_reset_starred).setVisible(false);
}
super.onCreateOptionsMenu(menu, inflater);
}
Expand Down Expand Up @@ -337,6 +349,51 @@ public boolean onOptionsItemSelected(MenuItem item) {
}
return true;
}
case R.id.menu_export_starred: {
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
android.support.v7.app.AlertDialog.Builder builder = new android.support.v7.app.AlertDialog.Builder(getActivity());
builder.setMessage(R.string.storage_request_explanation).setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialogInterface, int id) {
ActivityCompat.requestPermissions(getActivity(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSIONS_REQUEST_EXPORT_STARRED);
}
}).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialogInterface, int id) {
}
});
builder.show();
} else {
ActivityCompat.requestPermissions(getActivity(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSIONS_REQUEST_EXPORT_STARRED);
}
} else {
exportStarred();
}
return true;
}
case R.id.menu_reset_starred: {
// TODO MAYBE: ask for deletion of entries as well?
new android.support.v7.app.AlertDialog.Builder(getActivity())
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.context_menu_reset_starred)
.setMessage(R.string.context_menu_reset_starred_confirmation)
.setPositiveButton(R.string.confirm_positive, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new Thread() {
@Override
public void run() {
ContentResolver cr = MainApplication.getContext().getContentResolver();
String where = EntryColumns.WHERE_FAVORITE;
cr.update(mUri, FeedData.getUnstarredContentValues(), where, null);
}
}.start();
}

})
.setNegativeButton(R.string.confirm_negative, null)
.show();
return true;
}
case R.id.menu_refresh: {
downloadUnmobilitedEntries();
startRefresh();
Expand Down Expand Up @@ -397,6 +454,39 @@ private void downloadUnmobilitedEntries() {
}
}

private void exportStarred()
{
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
new Thread(new Runnable() {
@Override
public void run() {
try {
final String filename = Environment.getExternalStorageDirectory().toString() + "/spaRSS_starred" +
"_" + System.currentTimeMillis()+".html";
HTMLDigest.exportStarred(filename);
// display success message after export
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getActivity(), String.format(getString(R.string.message_exported_to), filename),
Toast.LENGTH_LONG).show();
}
});
} catch (Exception e) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getActivity(), R.string.error_feed_export, Toast.LENGTH_LONG).show();
}
});
}
}
}).start();
} else {
Toast.makeText(getActivity(), R.string.error_external_storage_not_available, Toast.LENGTH_LONG).show();
}
}

private void startRefresh() {
if (!PrefUtils.getBoolean(PrefUtils.IS_REFRESHING, false)) {
if (mUri != null && FeedDataContentProvider.URI_MATCHER.match(mUri) == FeedDataContentProvider.URI_ENTRIES_FOR_FEED) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ public static ContentValues getUnreadContentValues() {
return values;
}

public static ContentValues getUnstarredContentValues() {
ContentValues values = new ContentValues();
values.putNull(EntryColumns.IS_FAVORITE);
return values;
}

public static boolean shouldShowReadEntries(Uri uri) {
boolean alwaysShowRead = EntryColumns.FAVORITES_CONTENT_URI.equals(uri) || (FeedDataContentProvider.URI_MATCHER.match(uri) == FeedDataContentProvider.URI_SEARCH);
return alwaysShowRead || PrefUtils.getBoolean(PrefUtils.SHOW_READ, true);
Expand Down Expand Up @@ -190,6 +196,7 @@ public static class EntryColumns implements BaseColumns {
public static final String WHERE_READ = EntryColumns.IS_READ + Constants.DB_IS_TRUE;
public static final String WHERE_UNREAD = "(" + EntryColumns.IS_READ + Constants.DB_IS_NULL + Constants.DB_OR + EntryColumns.IS_READ + Constants.DB_IS_FALSE + ')';
public static final String WHERE_NOT_FAVORITE = "(" + EntryColumns.IS_FAVORITE + Constants.DB_IS_NULL + Constants.DB_OR + EntryColumns.IS_FAVORITE + Constants.DB_IS_FALSE + ')';
public static final String WHERE_FAVORITE = "(" + EntryColumns.IS_FAVORITE + Constants.DB_IS_TRUE + ')';

public static Uri ENTRIES_FOR_FEED_CONTENT_URI(String feedId) {
return Uri.parse(CONTENT_AUTHORITY + "/feeds/" + feedId + "/entries");
Expand Down
111 changes: 111 additions & 0 deletions mobile/src/main/java/net/etuldan/sparss/utils/HTMLDigest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package net.etuldan.sparss.utils;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.os.Environment;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.widget.Toast;

import net.etuldan.sparss.Constants;
import net.etuldan.sparss.MainApplication;
import net.etuldan.sparss.provider.FeedData;
import net.etuldan.sparss.provider.FeedDataContentProvider;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Date;

import static net.etuldan.sparss.MainApplication.getContext;

/**
* @author Oliver G
*/

public class HTMLDigest {

// TODO: Add timestamp to filename
public static final String STARRED_DIGEST = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/spaRSS_starred_digest.html";

private static final String[] FAVEXPORT_PROJECTION = new String[]{FeedData.EntryColumns.FEED_ID,
FeedData.EntryColumns.TITLE, FeedData.EntryColumns.DATE, FeedData.EntryColumns.LINK,
FeedData.EntryColumns.AUTHOR, FeedData.EntryColumns.ABSTRACT,
FeedData.EntryColumns.MOBILIZED_HTML};

private static final String FAVEXPORT_START = "<!DOCTYPE HTML><html>\n\t<head>\n\t\t<meta http-equiv='Content-Type' content='text/html; charset=UTF-8' />"
+ "<meta id='Viewport' name='viewport' content='initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no'>"
+ "\n\t\t<title>spaRSS favorites export</title>\n\t</head>\n\t<body>\n";
private static final String FAVEXPORT_ENTRY_START = "\t\t<article>\n\t\t\t<header>\n\t\t\t\t<h1>";
private static final String FAVEXPORT_ENTRY_AFTER_TITLE = "</h1>\n\t\t\t</header>\n\t\t\t<p><i>";
private static final String FAVEXPORT_ENTRY_AFTER_META = "</i>\n";
private static final String FAVEXPORT_ENTRY_LINK_START = "&lt;";
private static final String FAVEXPORT_ENTRY_LINK_END = "&gt;<br />";
private static final String FAVEXPORT_ENTRY_CLOSING ="</p>\n\t\t</article>\n";
private static final String FAVEXPORT_CLOSING = "</body>\n</html>\n";

// private static boolean mIncludeImages = false;

public static void exportStarred(String filename) throws IOException {

// Build HTML File
// HEADER
StringBuilder builder = new StringBuilder(FAVEXPORT_START);

// TODO: sort order: by Groups > by Feed > by Date
Cursor cursor = getContext().getContentResolver()
.query(FeedData.EntryColumns.FAVORITES_CONTENT_URI, FAVEXPORT_PROJECTION, null, null, FeedData.EntryColumns.DATE);

// no favorites at all? Display toast and leave!
if (cursor.getCount() == 0) {
Context context = getContext();
CharSequence text = "Favorites empty, nothing to export.";
int duration = Toast.LENGTH_SHORT;

Toast toast = Toast.makeText(context, text, duration);
toast.show();
return;
}

// Loop through all starred entries
// TODO:
// - Sort by Group and Feed Order Within Groups
// - Print Headlines with Group and Feed Names
while (cursor.moveToNext()) {
builder.append(FAVEXPORT_ENTRY_START);
builder.append(cursor.isNull(1) ? "" : TextUtils.htmlEncode(cursor.getString(1))); // title
builder.append(FAVEXPORT_ENTRY_AFTER_TITLE);

Date date = new Date(cursor.getLong(2));
Context context = getContext();
StringBuilder dateStringBuilder = new StringBuilder(DateFormat.getDateFormat(context).format(date)).append(' ').append(
DateFormat.getTimeFormat(context).format(date));

builder.append(dateStringBuilder);

if (!cursor.isNull(4)) {
builder.append(", ");
builder.append(cursor.getString(4)); // author if exists
}

String url = cursor.getString(3);
builder.append("<br />").append(FAVEXPORT_ENTRY_LINK_START);
builder.append("<a href='").append(url).append("'>");
builder.append(url).append("</a>").append(FAVEXPORT_ENTRY_LINK_END);
builder.append(FAVEXPORT_ENTRY_AFTER_META);
builder.append(cursor.isNull(6) ? cursor.getString(5) : cursor.getString(6)); // fulltext unavailable? use abstract!
builder.append(FAVEXPORT_ENTRY_CLOSING);
}

// CLOSING
builder.append(FAVEXPORT_CLOSING);

// Write File
BufferedWriter writer = new BufferedWriter(new FileWriter(filename));

writer.write(builder.toString());
writer.close();
}
}
8 changes: 8 additions & 0 deletions mobile/src/main/res/menu/entry_list.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@
android:icon="@drawable/ic_share"
android:title="@string/context_menu_share_starred"
app:showAsAction="always"/>
<item
android:id="@+id/menu_export_starred"
android:icon="@drawable/action_export"
android:title="@string/context_menu_export_starred" />
<item
android:id="@+id/menu_reset_starred"
android:icon="@drawable/action_export"
android:title="@string/context_menu_reset_starred" />
<item
android:id="@+id/menu_all_read"
android:icon="@drawable/ic_check"
Expand Down
13 changes: 9 additions & 4 deletions mobile/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
<string name="error_feed_process">Der Feed kann nicht bearbeitet werden.</string>
<string name="error_feed_url_exists">Diese Adresse existiert schon.</string>
<string name="error_feed_import">Die ausgewählte Datei konnte nicht importiert werden.</string>
<string name="error_feed_export">Der Export ist fehlgeschlagen. Stelle sicher, dass du eine beschreibbare SD-Karte verfügbar ist.</string>
<string name="error_feed_export">Der Export ist fehlgeschlagen. Stelle sicher, dass eine beschreibbare SD-Karte verfügbar ist.</string>
<string name="error_external_storage_not_available">Der externe Speicher (z.B. SD-Karte) ist nicht verfügbar.</string>
<string name="feed_url">URL</string>
<string name="optional">(optional)</string>
Expand Down Expand Up @@ -114,7 +114,7 @@
<string name="favorites">Favorisiert</string>
<string name="context_menu_hide_read">Gelesene Einträge verstecken</string>
<string name="context_menu_show_read">Gelesene Einträge anzeigen</string>
<string name="context_menu_share_starred">Favorisierte Einträge teilen</string>
<string name="context_menu_share_starred">Favoriten teilen</string>
<string name="menu_star">Favorisieren</string>
<string name="menu_unstar">Entfavorisieren</string>
<string name="menu_share">Teilen</string>
Expand Down Expand Up @@ -248,6 +248,11 @@
<string name="filter_help">Filter Hilfe</string>
<string name="settings_show_new_entries_description">Wenn deaktiviert, ist das Antippen des Buttons Neue Einträge erforderlich, um neue Einträge zu sehen</string>
<string name="google_news_custom_topic">Eigenes Thema</string>
<string name="storage_request_explanation">Um Feeds aus einer Datei importieren oder in eine Datei exportieren zu können, benötigt die App Zugriffsrechte auf die SD-Karte.</string>
<string name="storage_request_explanation">Um aus einer Datei importieren oder in eine Datei exportieren zu können, benötigt die App Zugriffsrechte auf die SD-Karte.</string>
<string name="feed_no_summary">&lt;Keine Zusammenfassung verfügbar&gt;</string>
</resources>
<string name="context_menu_export_starred">In HTML exportieren</string>
<string name="context_menu_reset_starred">Favoriten zurücksetzen</string>
<string name="context_menu_reset_starred_confirmation">Alle Markierungen entfernen?</string>
<string name="confirm_negative">Nein</string>
<string name="confirm_positive">Ja</string>
</resources>
9 changes: 7 additions & 2 deletions mobile/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,13 @@
<string name="passwordhttpAuth">Mot de passe</string>
<string name="settings_show_new_entries_description">Si désactivé, un appui sur le bouton Afficher nouveaux articles sera requis pour les afficher</string>
<string name="filter_help">Aide à propos des filtres</string>
<string name="storage_request_explanation">Pour pouvoir importer ou exporter les flux depuis ou vers un fichier, vous devez autoriser l\'application à accéder au stockage interne.</string>
<string name="storage_request_explanation">Pour pouvoir importer ou exporter depuis ou vers un fichier, vous devez autoriser l\'application à accéder au stockage interne.</string>
<string name="feed_no_summary">&lt; Aucun résumé disponible. &gt;</string>
<string name="google_news_custom_topic">Entrer un thème personnalisé</string>
<string name="context_menu_export_starred">Exporter vers HTML</string>
<string name="context_menu_reset_starred">Réinitialiser les favoris</string>
<string name="context_menu_reset_starred_confirmation">Éliminer tous les marques?</string>
<string name="confirm_negative">Non</string>
<string name="confirm_positive">Oui</string>

</resources>
</resources>
Loading