mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-06 15:58:39 +09:00
pt 1 in reviving the android build (copied from pm 28a1, wish me luck)
This commit is contained in:
parent
efa9662725
commit
d7788a6d6d
4249 changed files with 468189 additions and 0 deletions
|
|
@ -0,0 +1,89 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.BrowserApp;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.delegates.BrowserAppDelegate;
|
||||
import org.mozilla.gecko.mozglue.SafeIntent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* BrowserAppDelegate implementation that takes care of handling intents from content notifications.
|
||||
*/
|
||||
public class ContentNotificationsDelegate extends BrowserAppDelegate {
|
||||
// The application is opened from a content notification
|
||||
public static final String ACTION_CONTENT_NOTIFICATION = AppConstants.ANDROID_PACKAGE_NAME + ".action.CONTENT_NOTIFICATION";
|
||||
|
||||
public static final String EXTRA_READ_BUTTON = "read_button";
|
||||
public static final String EXTRA_URLS = "urls";
|
||||
|
||||
private static final String TELEMETRY_EXTRA_CONTENT_UPDATE = "content_update";
|
||||
private static final String TELEMETRY_EXTRA_READ_NOW_BUTTON = TELEMETRY_EXTRA_CONTENT_UPDATE + "_read_now";
|
||||
|
||||
@Override
|
||||
public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
|
||||
if (savedInstanceState != null) {
|
||||
// This activity is getting restored: We do not want to handle the URLs in the Intent again. The browser
|
||||
// will take care of restoring the tabs we already created.
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
final Intent unsafeIntent = browserApp.getIntent();
|
||||
|
||||
// Nothing to do.
|
||||
if (unsafeIntent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final SafeIntent intent = new SafeIntent(unsafeIntent);
|
||||
|
||||
if (ACTION_CONTENT_NOTIFICATION.equals(intent.getAction())) {
|
||||
openURLsFromIntent(browserApp, intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(BrowserApp browserApp, @NonNull final SafeIntent intent) {
|
||||
if (ACTION_CONTENT_NOTIFICATION.equals(intent.getAction())) {
|
||||
openURLsFromIntent(browserApp, intent);
|
||||
}
|
||||
}
|
||||
|
||||
private void openURLsFromIntent(BrowserApp browserApp, @NonNull final SafeIntent intent) {
|
||||
final List<String> urls = intent.getStringArrayListExtra(EXTRA_URLS);
|
||||
if (urls != null) {
|
||||
browserApp.openUrls(urls);
|
||||
}
|
||||
|
||||
Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(browserApp));
|
||||
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, TELEMETRY_EXTRA_CONTENT_UPDATE);
|
||||
|
||||
if (intent.getBooleanExtra(EXTRA_READ_BUTTON, false)) {
|
||||
// "READ NOW" button in notification was clicked
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, TELEMETRY_EXTRA_READ_NOW_BUTTON);
|
||||
|
||||
// Android's "auto cancel" won't remove the notification when an action button is pressed. So we do it ourselves here.
|
||||
NotificationManagerCompat.from(browserApp).cancel(R.id.websiteContentNotification);
|
||||
} else {
|
||||
// Notification was clicked
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, TELEMETRY_EXTRA_CONTENT_UPDATE);
|
||||
}
|
||||
|
||||
Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(browserApp));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.v4.content.WakefulBroadcastReceiver;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Broadcast receiver that will receive broadcasts from the AlarmManager and start the FeedService
|
||||
* with the given action.
|
||||
*/
|
||||
public class FeedAlarmReceiver extends WakefulBroadcastReceiver {
|
||||
private static final String LOGTAG = "FeedCheckAction";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
final String action = intent.getAction();
|
||||
|
||||
Log.d(LOGTAG, "Received alarm with action: " + action);
|
||||
|
||||
final Intent serviceIntent = new Intent(context, FeedService.class);
|
||||
serviceIntent.setAction(action);
|
||||
|
||||
startWakefulService(context, serviceIntent);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.mozilla.gecko.feeds.parser.Feed;
|
||||
import org.mozilla.gecko.feeds.parser.SimpleFeedParser;
|
||||
import org.mozilla.gecko.util.IOUtils;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import ch.boye.httpclientandroidlib.util.TextUtils;
|
||||
|
||||
/**
|
||||
* Helper class for fetching and parsing a feed.
|
||||
*/
|
||||
public class FeedFetcher {
|
||||
private static final int CONNECT_TIMEOUT = 15000;
|
||||
private static final int READ_TIMEOUT = 15000;
|
||||
|
||||
public static class FeedResponse {
|
||||
public final Feed feed;
|
||||
public final String etag;
|
||||
public final String lastModified;
|
||||
|
||||
public FeedResponse(Feed feed, String etag, String lastModified) {
|
||||
this.feed = feed;
|
||||
this.etag = etag;
|
||||
this.lastModified = lastModified;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and parse a feed from the given URL. Will return null if fetching or parsing failed.
|
||||
*/
|
||||
public static FeedResponse fetchAndParseFeed(String url) {
|
||||
return fetchAndParseFeedIfModified(url, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and parse a feed from the given URL using the given ETag and "Last modified" value.
|
||||
*
|
||||
* Will return null if fetching or parsing failed. Will also return null if the feed has not
|
||||
* changed (ETag / Last-Modified-Since).
|
||||
*
|
||||
* @param eTag The ETag from the last fetch or null if no ETag is available (will always fetch feed)
|
||||
* @param lastModified The "Last modified" header from the last time the feed has been fetch or
|
||||
* null if no value is available (will always fetch feed)
|
||||
* @return A FeedResponse or null if no feed could be fetched (error or no new version available)
|
||||
*/
|
||||
@Nullable
|
||||
public static FeedResponse fetchAndParseFeedIfModified(@NonNull String url, @Nullable String eTag, @Nullable String lastModified) {
|
||||
HttpURLConnection connection = null;
|
||||
InputStream stream = null;
|
||||
|
||||
try {
|
||||
connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setInstanceFollowRedirects(true);
|
||||
connection.setConnectTimeout(CONNECT_TIMEOUT);
|
||||
connection.setReadTimeout(READ_TIMEOUT);
|
||||
|
||||
if (!TextUtils.isEmpty(eTag)) {
|
||||
connection.setRequestProperty("If-None-Match", eTag);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(lastModified)) {
|
||||
connection.setRequestProperty("If-Modified-Since", lastModified);
|
||||
}
|
||||
|
||||
final int statusCode = connection.getResponseCode();
|
||||
|
||||
if (statusCode != HttpURLConnection.HTTP_OK) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String responseEtag = connection.getHeaderField("ETag");
|
||||
if (!TextUtils.isEmpty(responseEtag) && responseEtag.startsWith("W/")) {
|
||||
// Weak ETag, get actual ETag value
|
||||
responseEtag = responseEtag.substring(2);
|
||||
}
|
||||
|
||||
final String updatedLastModified = connection.getHeaderField("Last-Modified");
|
||||
|
||||
stream = new BufferedInputStream(connection.getInputStream());
|
||||
|
||||
final SimpleFeedParser parser = new SimpleFeedParser();
|
||||
final Feed feed = parser.parse(stream);
|
||||
|
||||
return new FeedResponse(feed, responseEtag, updatedLastModified);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
} catch (SimpleFeedParser.ParserException e) {
|
||||
return null;
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
IOUtils.safeStreamClose(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.net.ConnectivityManagerCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import com.keepsafe.switchboard.SwitchBoard;
|
||||
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.GeckoProfile;
|
||||
import org.mozilla.gecko.GeckoSharedPrefs;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.feeds.action.FeedAction;
|
||||
import org.mozilla.gecko.feeds.action.CheckForUpdatesAction;
|
||||
import org.mozilla.gecko.feeds.action.EnrollSubscriptionsAction;
|
||||
import org.mozilla.gecko.feeds.action.SetupAlarmsAction;
|
||||
import org.mozilla.gecko.feeds.action.SubscribeToFeedAction;
|
||||
import org.mozilla.gecko.feeds.action.WithdrawSubscriptionsAction;
|
||||
import org.mozilla.gecko.preferences.GeckoPreferences;
|
||||
import org.mozilla.gecko.Experiments;
|
||||
|
||||
/**
|
||||
* Background service for subscribing to and checking website feeds to notify the user about updates.
|
||||
*/
|
||||
public class FeedService extends IntentService {
|
||||
private static final String LOGTAG = "GeckoFeedService";
|
||||
|
||||
public static final String ACTION_SETUP = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.SETUP";
|
||||
public static final String ACTION_SUBSCRIBE = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.SUBSCRIBE";
|
||||
public static final String ACTION_CHECK = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.CHECK";
|
||||
public static final String ACTION_ENROLL = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.ENROLL";
|
||||
public static final String ACTION_WITHDRAW = AppConstants.ANDROID_PACKAGE_NAME + ".FEEDS.WITHDRAW";
|
||||
|
||||
public static void setup(Context context) {
|
||||
Intent intent = new Intent(context, FeedService.class);
|
||||
intent.setAction(ACTION_SETUP);
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
public static void subscribe(Context context, String feedUrl) {
|
||||
Intent intent = new Intent(context, FeedService.class);
|
||||
intent.setAction(ACTION_SUBSCRIBE);
|
||||
intent.putExtra(SubscribeToFeedAction.EXTRA_FEED_URL, feedUrl);
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
public FeedService() {
|
||||
super(LOGTAG);
|
||||
}
|
||||
|
||||
private BrowserDB browserDB;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
browserDB = BrowserDB.from(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
try {
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(LOGTAG, "Service started with action: " + intent.getAction());
|
||||
|
||||
if (!isInExperiment(this)) {
|
||||
Log.d(LOGTAG, "Not in content notifications experiment. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
FeedAction action = createActionForIntent(intent);
|
||||
if (action == null) {
|
||||
Log.d(LOGTAG, "No action to process");
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.requiresPreferenceEnabled() && !isPreferenceEnabled()) {
|
||||
Log.d(LOGTAG, "Preference is disabled. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.requiresNetwork() && !isConnectedToUnmeteredNetwork()) {
|
||||
// For now just skip if we are not connected or the network is metered. We do not want
|
||||
// to use precious mobile traffic.
|
||||
Log.d(LOGTAG, "Not connected to a network or network is metered. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
action.perform(browserDB, intent);
|
||||
} finally {
|
||||
FeedAlarmReceiver.completeWakefulIntent(intent);
|
||||
}
|
||||
|
||||
Log.d(LOGTAG, "Done.");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private FeedAction createActionForIntent(Intent intent) {
|
||||
final Context context = getApplicationContext();
|
||||
|
||||
switch (intent.getAction()) {
|
||||
case ACTION_SETUP:
|
||||
return new SetupAlarmsAction(context);
|
||||
|
||||
case ACTION_SUBSCRIBE:
|
||||
return new SubscribeToFeedAction(context);
|
||||
|
||||
case ACTION_CHECK:
|
||||
return new CheckForUpdatesAction(context);
|
||||
|
||||
case ACTION_ENROLL:
|
||||
return new EnrollSubscriptionsAction(context);
|
||||
|
||||
case ACTION_WITHDRAW:
|
||||
return new WithdrawSubscriptionsAction(context);
|
||||
|
||||
default:
|
||||
throw new AssertionError("Unknown action: " + intent.getAction());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isConnectedToUnmeteredNetwork() {
|
||||
ConnectivityManager manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
|
||||
if (networkInfo == null || !networkInfo.isConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !ConnectivityManagerCompat.isActiveNetworkMetered(manager);
|
||||
}
|
||||
|
||||
public static boolean isInExperiment(Context context) {
|
||||
return SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_12HRS) ||
|
||||
SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_5PM) ||
|
||||
SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_8AM);
|
||||
}
|
||||
|
||||
public static String getEnabledExperiment(Context context) {
|
||||
String experiment = null;
|
||||
|
||||
if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_12HRS)) {
|
||||
experiment = Experiments.CONTENT_NOTIFICATIONS_12HRS;
|
||||
} else if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_8AM)) {
|
||||
experiment = Experiments.CONTENT_NOTIFICATIONS_8AM;
|
||||
} else if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_5PM)) {
|
||||
experiment = Experiments.CONTENT_NOTIFICATIONS_5PM;
|
||||
}
|
||||
|
||||
return experiment;
|
||||
}
|
||||
|
||||
private boolean isPreferenceEnabled() {
|
||||
return GeckoSharedPrefs.forApp(this).getBoolean(GeckoPreferences.PREFS_NOTIFICATIONS_CONTENT, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds.action;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.text.format.DateFormat;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.BrowserApp;
|
||||
import org.mozilla.gecko.GeckoApp;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.db.UrlAnnotations;
|
||||
import org.mozilla.gecko.feeds.ContentNotificationsDelegate;
|
||||
import org.mozilla.gecko.feeds.FeedFetcher;
|
||||
import org.mozilla.gecko.feeds.FeedService;
|
||||
import org.mozilla.gecko.feeds.parser.Feed;
|
||||
import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
|
||||
import org.mozilla.gecko.preferences.GeckoPreferences;
|
||||
import org.mozilla.gecko.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* CheckForUpdatesAction: Check if feeds we subscribed to have new content available.
|
||||
*/
|
||||
public class CheckForUpdatesAction extends FeedAction {
|
||||
/**
|
||||
* This extra will be added to Intents fired by the notification.
|
||||
*/
|
||||
public static final String EXTRA_CONTENT_NOTIFICATION = "content-notification";
|
||||
|
||||
private final Context context;
|
||||
|
||||
public CheckForUpdatesAction(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(BrowserDB browserDB, Intent intent) {
|
||||
final UrlAnnotations urlAnnotations = browserDB.getUrlAnnotations();
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
final List<Feed> updatedFeeds = new ArrayList<>();
|
||||
|
||||
log("Checking feeds for updates..");
|
||||
|
||||
Cursor cursor = urlAnnotations.getFeedSubscriptions(resolver);
|
||||
if (cursor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
FeedSubscription subscription = FeedSubscription.fromCursor(cursor);
|
||||
|
||||
FeedFetcher.FeedResponse response = checkFeedForUpdates(subscription);
|
||||
if (response != null) {
|
||||
final Feed feed = response.feed;
|
||||
|
||||
if (!hasBeenVisited(browserDB, feed.getLastItem().getURL())) {
|
||||
// Only notify about this update if the last item hasn't been visited yet.
|
||||
updatedFeeds.add(feed);
|
||||
} else {
|
||||
Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL,
|
||||
TelemetryContract.Method.SERVICE,
|
||||
"content_update");
|
||||
Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
|
||||
}
|
||||
|
||||
urlAnnotations.updateFeedSubscription(resolver, subscription);
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
log("Could not deserialize subscription", e);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
showNotification(updatedFeeds);
|
||||
}
|
||||
|
||||
private FeedFetcher.FeedResponse checkFeedForUpdates(FeedSubscription subscription) {
|
||||
log("Checking feed: " + subscription.getFeedTitle());
|
||||
|
||||
FeedFetcher.FeedResponse response = fetchFeed(subscription);
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (subscription.hasBeenUpdated(response)) {
|
||||
log("* Feed has changed. New item: " + response.feed.getLastItem().getTitle());
|
||||
|
||||
subscription.update(response);
|
||||
|
||||
return response;
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this URL has been visited before.
|
||||
*
|
||||
* We do an exact match. So this can fail if the feed uses a different URL and redirects to
|
||||
* content. But it's better than no checks at all.
|
||||
*/
|
||||
private boolean hasBeenVisited(final BrowserDB browserDB, final String url) {
|
||||
final Cursor cursor = browserDB.getHistoryForURL(context.getContentResolver(), url);
|
||||
if (cursor == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getInt(cursor.getColumnIndex(BrowserContract.History.VISITS)) > 0;
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void showNotification(List<Feed> updatedFeeds) {
|
||||
final int feedCount = updatedFeeds.size();
|
||||
if (feedCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (feedCount == 1) {
|
||||
showNotificationForSingleUpdate(updatedFeeds.get(0));
|
||||
} else {
|
||||
showNotificationForMultipleUpdates(updatedFeeds);
|
||||
}
|
||||
|
||||
Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.NOTIFICATION, "content_update");
|
||||
Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
|
||||
}
|
||||
|
||||
private void showNotificationForSingleUpdate(Feed feed) {
|
||||
final String date = DateFormat.getMediumDateFormat(context).format(new Date(feed.getLastItem().getTimestamp()));
|
||||
|
||||
final NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle()
|
||||
.bigText(feed.getLastItem().getTitle())
|
||||
.setBigContentTitle(feed.getTitle())
|
||||
.setSummaryText(context.getString(R.string.content_notification_updated_on, date));
|
||||
|
||||
final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, createOpenIntent(feed), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
final Notification notification = new NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_status_logo)
|
||||
.setContentTitle(feed.getTitle())
|
||||
.setContentText(feed.getLastItem().getTitle())
|
||||
.setStyle(style)
|
||||
.setColor(ContextCompat.getColor(context, R.color.fennec_ui_orange))
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.addAction(createOpenAction(feed))
|
||||
.addAction(createNotificationSettingsAction())
|
||||
.build();
|
||||
|
||||
NotificationManagerCompat.from(context).notify(R.id.websiteContentNotification, notification);
|
||||
}
|
||||
|
||||
private void showNotificationForMultipleUpdates(List<Feed> feeds) {
|
||||
final NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
|
||||
for (Feed feed : feeds) {
|
||||
inboxStyle.addLine(StringUtils.stripScheme(feed.getLastItem().getURL(), StringUtils.UrlFlags.STRIP_HTTPS));
|
||||
}
|
||||
inboxStyle.setSummaryText(context.getString(R.string.content_notification_summary));
|
||||
|
||||
final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, createOpenIntent(feeds), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_status_logo)
|
||||
.setContentTitle(context.getString(R.string.content_notification_title_plural, feeds.size()))
|
||||
.setContentText(context.getString(R.string.content_notification_summary))
|
||||
.setStyle(inboxStyle)
|
||||
.setColor(ContextCompat.getColor(context, R.color.fennec_ui_orange))
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.addAction(createOpenAction(feeds))
|
||||
.setNumber(feeds.size())
|
||||
.addAction(createNotificationSettingsAction())
|
||||
.build();
|
||||
|
||||
NotificationManagerCompat.from(context).notify(R.id.websiteContentNotification, notification);
|
||||
}
|
||||
|
||||
private Intent createOpenIntent(Feed feed) {
|
||||
final List<Feed> feeds = new ArrayList<>();
|
||||
feeds.add(feed);
|
||||
|
||||
return createOpenIntent(feeds);
|
||||
}
|
||||
|
||||
private Intent createOpenIntent(List<Feed> feeds) {
|
||||
final ArrayList<String> urls = new ArrayList<>();
|
||||
for (Feed feed : feeds) {
|
||||
urls.add(feed.getLastItem().getURL());
|
||||
}
|
||||
|
||||
final Intent intent = new Intent(context, BrowserApp.class);
|
||||
intent.setAction(ContentNotificationsDelegate.ACTION_CONTENT_NOTIFICATION);
|
||||
intent.putStringArrayListExtra(ContentNotificationsDelegate.EXTRA_URLS, urls);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
private NotificationCompat.Action createOpenAction(Feed feed) {
|
||||
final List<Feed> feeds = new ArrayList<>();
|
||||
feeds.add(feed);
|
||||
|
||||
return createOpenAction(feeds);
|
||||
}
|
||||
|
||||
private NotificationCompat.Action createOpenAction(List<Feed> feeds) {
|
||||
Intent intent = createOpenIntent(feeds);
|
||||
intent.putExtra(ContentNotificationsDelegate.EXTRA_READ_BUTTON, true);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
return new NotificationCompat.Action(
|
||||
R.drawable.open_in_browser,
|
||||
context.getString(R.string.content_notification_action_read_now),
|
||||
pendingIntent);
|
||||
}
|
||||
|
||||
private NotificationCompat.Action createNotificationSettingsAction() {
|
||||
final Intent intent = new Intent(GeckoApp.ACTION_LAUNCH_SETTINGS);
|
||||
intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
|
||||
intent.putExtra(EXTRA_CONTENT_NOTIFICATION, true);
|
||||
|
||||
GeckoPreferences.setResourceToOpen(intent, "preferences_notifications");
|
||||
|
||||
PendingIntent settingsIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
return new NotificationCompat.Action(
|
||||
R.drawable.settings_notifications,
|
||||
context.getString(R.string.content_notification_action_settings),
|
||||
settingsIntent);
|
||||
}
|
||||
|
||||
private FeedFetcher.FeedResponse fetchFeed(FeedSubscription subscription) {
|
||||
return FeedFetcher.fetchAndParseFeedIfModified(
|
||||
subscription.getFeedUrl(),
|
||||
subscription.getETag(),
|
||||
subscription.getLastModified()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresNetwork() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresPreferenceEnabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds.action;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.db.UrlAnnotations;
|
||||
import org.mozilla.gecko.feeds.FeedService;
|
||||
import org.mozilla.gecko.feeds.knownsites.KnownSiteBlogger;
|
||||
import org.mozilla.gecko.feeds.knownsites.KnownSite;
|
||||
import org.mozilla.gecko.feeds.knownsites.KnownSiteMedium;
|
||||
import org.mozilla.gecko.feeds.knownsites.KnownSiteTumblr;
|
||||
import org.mozilla.gecko.feeds.knownsites.KnownSiteWordpress;
|
||||
|
||||
/**
|
||||
* EnrollSubscriptionsAction: Search for bookmarks of known sites we can subscribe to.
|
||||
*/
|
||||
public class EnrollSubscriptionsAction extends FeedAction {
|
||||
private static final String LOGTAG = "FeedEnrollAction";
|
||||
|
||||
private static final KnownSite[] knownSites = {
|
||||
new KnownSiteMedium(),
|
||||
new KnownSiteBlogger(),
|
||||
new KnownSiteWordpress(),
|
||||
new KnownSiteTumblr(),
|
||||
};
|
||||
|
||||
private Context context;
|
||||
|
||||
public EnrollSubscriptionsAction(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(BrowserDB db, Intent intent) {
|
||||
log("Searching for bookmarks to enroll in updates");
|
||||
|
||||
final ContentResolver contentResolver = context.getContentResolver();
|
||||
|
||||
for (KnownSite knownSite : knownSites) {
|
||||
searchFor(db, contentResolver, knownSite);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresNetwork() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresPreferenceEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void searchFor(BrowserDB db, ContentResolver contentResolver, KnownSite knownSite) {
|
||||
final UrlAnnotations urlAnnotations = db.getUrlAnnotations();
|
||||
|
||||
final Cursor cursor = db.getBookmarksForPartialUrl(contentResolver, knownSite.getURLSearchString());
|
||||
if (cursor == null) {
|
||||
log("Nothing found (" + knownSite.getClass().getSimpleName() + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log("Found " + cursor.getCount() + " websites");
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
|
||||
final String url = cursor.getString(cursor.getColumnIndex(BrowserContract.Bookmarks.URL));
|
||||
|
||||
log(" URL: " + url);
|
||||
|
||||
String feedUrl = knownSite.getFeedFromURL(url);
|
||||
if (TextUtils.isEmpty(feedUrl)) {
|
||||
log("Could not determine feed for URL: " + url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!urlAnnotations.hasFeedUrlForWebsite(contentResolver, url)) {
|
||||
urlAnnotations.insertFeedUrl(contentResolver, url, feedUrl);
|
||||
}
|
||||
|
||||
if (!urlAnnotations.hasFeedSubscription(contentResolver, feedUrl)) {
|
||||
FeedService.subscribe(context, feedUrl);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds.action;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
|
||||
/**
|
||||
* Interface for actions run by FeedService.
|
||||
*/
|
||||
public abstract class FeedAction {
|
||||
public static final boolean DEBUG_LOG = false;
|
||||
|
||||
/**
|
||||
* Perform this action.
|
||||
*
|
||||
* @param browserDB database instance to perform the action.
|
||||
* @param intent used to start the service.
|
||||
*/
|
||||
public abstract void perform(BrowserDB browserDB, Intent intent);
|
||||
|
||||
/**
|
||||
* Does this action require an active network connection?
|
||||
*/
|
||||
public abstract boolean requiresNetwork();
|
||||
|
||||
/**
|
||||
* Should this action only run if the preference is enabled?
|
||||
*/
|
||||
public abstract boolean requiresPreferenceEnabled();
|
||||
|
||||
/**
|
||||
* This method will swallow all log messages to avoid logging potential personal information.
|
||||
*
|
||||
* For debugging purposes set {@code DEBUG_LOG} to true.
|
||||
*/
|
||||
public void log(String message) {
|
||||
if (DEBUG_LOG) {
|
||||
Log.d("Gecko" + getClass().getSimpleName(), message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will swallow all log messages to avoid logging potential personal information.
|
||||
*
|
||||
* For debugging purposes set {@code DEBUG_LOG} to true.
|
||||
*/
|
||||
public void log(String message, Throwable throwable) {
|
||||
if (DEBUG_LOG) {
|
||||
Log.d("Gecko" + getClass().getSimpleName(), message, throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds.action;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.keepsafe.switchboard.SwitchBoard;
|
||||
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.feeds.FeedAlarmReceiver;
|
||||
import org.mozilla.gecko.feeds.FeedService;
|
||||
import org.mozilla.gecko.Experiments;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Calendar;
|
||||
|
||||
/**
|
||||
* SetupAlarmsAction: Set up alarms to run various actions every now and then.
|
||||
*/
|
||||
public class SetupAlarmsAction extends FeedAction {
|
||||
private static final String LOGTAG = "FeedSetupAction";
|
||||
|
||||
private Context context;
|
||||
|
||||
public SetupAlarmsAction(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(BrowserDB browserDB, Intent intent) {
|
||||
final AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
|
||||
cancelPreviousAlarms(alarmManager);
|
||||
scheduleAlarms(alarmManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresNetwork() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresPreferenceEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void cancelPreviousAlarms(AlarmManager alarmManager) {
|
||||
final PendingIntent withdrawIntent = getWithdrawPendingIntent();
|
||||
alarmManager.cancel(withdrawIntent);
|
||||
|
||||
final PendingIntent enrollIntent = getEnrollPendingIntent();
|
||||
alarmManager.cancel(enrollIntent);
|
||||
|
||||
final PendingIntent checkIntent = getCheckPendingIntent();
|
||||
alarmManager.cancel(checkIntent);
|
||||
|
||||
log("Cancelled previous alarms");
|
||||
}
|
||||
|
||||
private void scheduleAlarms(AlarmManager alarmManager) {
|
||||
alarmManager.setInexactRepeating(
|
||||
AlarmManager.ELAPSED_REALTIME,
|
||||
SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_FIFTEEN_MINUTES,
|
||||
AlarmManager.INTERVAL_DAY,
|
||||
getWithdrawPendingIntent());
|
||||
|
||||
alarmManager.setInexactRepeating(
|
||||
AlarmManager.ELAPSED_REALTIME,
|
||||
SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR,
|
||||
AlarmManager.INTERVAL_DAY,
|
||||
getEnrollPendingIntent()
|
||||
);
|
||||
|
||||
if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_12HRS)) {
|
||||
scheduleUpdateCheckEvery12Hours(alarmManager);
|
||||
}
|
||||
|
||||
if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_8AM)) {
|
||||
scheduleUpdateAtFullHour(alarmManager, 8);
|
||||
}
|
||||
|
||||
if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_5PM)) {
|
||||
scheduleUpdateAtFullHour(alarmManager, 17);
|
||||
}
|
||||
|
||||
|
||||
log("Scheduled alarms");
|
||||
}
|
||||
|
||||
private void scheduleUpdateCheckEvery12Hours(AlarmManager alarmManager) {
|
||||
alarmManager.setInexactRepeating(
|
||||
AlarmManager.ELAPSED_REALTIME,
|
||||
SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HOUR,
|
||||
AlarmManager.INTERVAL_HALF_DAY,
|
||||
getCheckPendingIntent()
|
||||
);
|
||||
}
|
||||
|
||||
private void scheduleUpdateAtFullHour(AlarmManager alarmManager, int hourOfDay) {
|
||||
final Calendar calendar = Calendar.getInstance();
|
||||
|
||||
if (calendar.get(Calendar.HOUR_OF_DAY) >= hourOfDay) {
|
||||
// This time has already passed today. Try again tomorrow.
|
||||
calendar.add(Calendar.DAY_OF_MONTH, 1);
|
||||
}
|
||||
|
||||
calendar.set(Calendar.HOUR_OF_DAY, hourOfDay);
|
||||
calendar.set(Calendar.MINUTE, 0);
|
||||
calendar.set(Calendar.SECOND, 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
|
||||
alarmManager.setInexactRepeating(
|
||||
AlarmManager.RTC,
|
||||
calendar.getTimeInMillis(),
|
||||
AlarmManager.INTERVAL_DAY,
|
||||
getCheckPendingIntent()
|
||||
);
|
||||
|
||||
log("Scheduled update alarm at " + DateFormat.getDateTimeInstance().format(calendar.getTime()));
|
||||
}
|
||||
|
||||
private PendingIntent getWithdrawPendingIntent() {
|
||||
Intent intent = new Intent(context, FeedAlarmReceiver.class);
|
||||
intent.setAction(FeedService.ACTION_WITHDRAW);
|
||||
return PendingIntent.getBroadcast(context, 0, intent, 0);
|
||||
}
|
||||
|
||||
private PendingIntent getEnrollPendingIntent() {
|
||||
Intent intent = new Intent(context, FeedAlarmReceiver.class);
|
||||
intent.setAction(FeedService.ACTION_ENROLL);
|
||||
return PendingIntent.getBroadcast(context, 0, intent, 0);
|
||||
}
|
||||
|
||||
private PendingIntent getCheckPendingIntent() {
|
||||
Intent intent = new Intent(context, FeedAlarmReceiver.class);
|
||||
intent.setAction(FeedService.ACTION_CHECK);
|
||||
return PendingIntent.getBroadcast(context, 0, intent, 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds.action;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.db.UrlAnnotations;
|
||||
import org.mozilla.gecko.feeds.FeedFetcher;
|
||||
import org.mozilla.gecko.feeds.FeedService;
|
||||
import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
|
||||
|
||||
/**
|
||||
* SubscribeToFeedAction: Try to fetch a feed and create a subscription if successful.
|
||||
*/
|
||||
public class SubscribeToFeedAction extends FeedAction {
|
||||
private static final String LOGTAG = "FeedSubscribeAction";
|
||||
|
||||
public static final String EXTRA_FEED_URL = "feed_url";
|
||||
|
||||
private Context context;
|
||||
|
||||
public SubscribeToFeedAction(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(BrowserDB browserDB, Intent intent) {
|
||||
final UrlAnnotations urlAnnotations = browserDB.getUrlAnnotations();
|
||||
|
||||
final Bundle extras = intent.getExtras();
|
||||
final String feedUrl = extras.getString(EXTRA_FEED_URL);
|
||||
|
||||
if (urlAnnotations.hasFeedSubscription(context.getContentResolver(), feedUrl)) {
|
||||
log("Already subscribed to " + feedUrl + ". Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
log("Subscribing to feed: " + feedUrl);
|
||||
|
||||
subscribe(urlAnnotations, feedUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresNetwork() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresPreferenceEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void subscribe(UrlAnnotations urlAnnotations, String feedUrl) {
|
||||
FeedFetcher.FeedResponse response = FeedFetcher.fetchAndParseFeed(feedUrl);
|
||||
if (response == null) {
|
||||
log(String.format("Could not fetch feed (%s). Not subscribing for now.", feedUrl));
|
||||
return;
|
||||
}
|
||||
|
||||
log("Subscribing to feed: " + response.feed.getTitle());
|
||||
log(" Last item: " + response.feed.getLastItem().getTitle());
|
||||
|
||||
final FeedSubscription subscription = FeedSubscription.create(feedUrl, response);
|
||||
|
||||
urlAnnotations.insertFeedSubscription(context.getContentResolver(), subscription);
|
||||
|
||||
Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.SERVICE, "content_update");
|
||||
Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds.action;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.mozilla.gecko.Telemetry;
|
||||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.db.UrlAnnotations;
|
||||
import org.mozilla.gecko.feeds.FeedService;
|
||||
import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
|
||||
|
||||
/**
|
||||
* WithdrawSubscriptionsAction: Look for feeds to unsubscribe from.
|
||||
*/
|
||||
public class WithdrawSubscriptionsAction extends FeedAction {
|
||||
private static final String LOGTAG = "FeedWithdrawAction";
|
||||
|
||||
private Context context;
|
||||
|
||||
public WithdrawSubscriptionsAction(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(BrowserDB browserDB, Intent intent) {
|
||||
log("Searching for subscriptions to remove..");
|
||||
|
||||
final UrlAnnotations urlAnnotations = browserDB.getUrlAnnotations();
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
|
||||
removeFeedsOfUnknownUrls(browserDB, urlAnnotations, resolver);
|
||||
removeSubscriptionsOfRemovedFeeds(urlAnnotations, resolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for website URLs with a feed assigned. Remove entry if website URL is not known anymore:
|
||||
* For now this means the website is not bookmarked.
|
||||
*/
|
||||
private void removeFeedsOfUnknownUrls(BrowserDB browserDB, UrlAnnotations urlAnnotations, ContentResolver resolver) {
|
||||
Cursor cursor = urlAnnotations.getWebsitesWithFeedUrl(resolver);
|
||||
if (cursor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
final String url = cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.URL));
|
||||
|
||||
if (!browserDB.isBookmark(resolver, url)) {
|
||||
log("Removing feed for unknown URL: " + url);
|
||||
|
||||
urlAnnotations.deleteFeedUrl(resolver, url);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove subscriptions of feed URLs that are not assigned to a website URL (anymore).
|
||||
*/
|
||||
private void removeSubscriptionsOfRemovedFeeds(UrlAnnotations urlAnnotations, ContentResolver resolver) {
|
||||
Cursor cursor = urlAnnotations.getFeedSubscriptions(resolver);
|
||||
if (cursor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
final FeedSubscription subscription = FeedSubscription.fromCursor(cursor);
|
||||
|
||||
if (!urlAnnotations.hasWebsiteForFeedUrl(resolver, subscription.getFeedUrl())) {
|
||||
log("Removing subscription for feed: " + subscription.getFeedUrl());
|
||||
|
||||
urlAnnotations.deleteFeedSubscription(resolver, subscription);
|
||||
|
||||
Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
|
||||
Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.SERVICE, "content_update");
|
||||
Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
log("Could not deserialize subscription", e);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresNetwork() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresPreferenceEnabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds.knownsites;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A site we know and for which we can guess the feed URL from an arbitrary URL.
|
||||
*/
|
||||
public interface KnownSite {
|
||||
/**
|
||||
* Get a search string to find URLs of this site in our database. This search string is usually
|
||||
* a partial domain / URL.
|
||||
*
|
||||
* For example we could return "medium.com" to find all URLs that contain this string. This could
|
||||
* obviously find URLs that are not actually medium.com sites. This is acceptable as long as
|
||||
* getFeedFromURL() can handle these inputs and either returns a feed for valid URLs or null for
|
||||
* other matches that are not related to this site.
|
||||
*/
|
||||
@NonNull String getURLSearchString();
|
||||
|
||||
/**
|
||||
* Get the Feed URL for this URL. For a known site we can "guess" the feed URL from an URL
|
||||
* pointing to any page. The input URL will be a result from the database found with the value
|
||||
* returned by getURLSearchString().
|
||||
*
|
||||
* Example:
|
||||
* - Input: https://medium.com/@antlam/ux-thoughts-for-2016-1fc1d6e515e8
|
||||
* - Output: https://medium.com/feed/@antlam
|
||||
*
|
||||
* @return the url representing a feed, or null if a feed could not be determined.
|
||||
*/
|
||||
@Nullable String getFeedFromURL(String url);
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds.knownsites;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Blogger.com
|
||||
*/
|
||||
public class KnownSiteBlogger implements KnownSite {
|
||||
@Override
|
||||
public String getURLSearchString() {
|
||||
return ".blogspot.com";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFeedFromURL(String url) {
|
||||
Pattern pattern = Pattern.compile("https?://(www\\.)?(.*?)\\.blogspot\\.com(/.*)?");
|
||||
Matcher matcher = pattern.matcher(url);
|
||||
if (matcher.matches()) {
|
||||
return String.format("https://%s.blogspot.com/feeds/posts/default", matcher.group(2));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds.knownsites;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Medium.com
|
||||
*/
|
||||
public class KnownSiteMedium implements KnownSite {
|
||||
@Override
|
||||
public String getURLSearchString() {
|
||||
return "://medium.com/";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFeedFromURL(String url) {
|
||||
Pattern pattern = Pattern.compile("https?://medium.com/([^/]+)(/.*)?");
|
||||
Matcher matcher = pattern.matcher(url);
|
||||
if (matcher.matches()) {
|
||||
return String.format("https://medium.com/feed/%s", matcher.group(1));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds.knownsites;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Tumblr.com
|
||||
*/
|
||||
public class KnownSiteTumblr implements KnownSite {
|
||||
@Override
|
||||
public String getURLSearchString() {
|
||||
return ".tumblr.com";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFeedFromURL(String url) {
|
||||
final Pattern pattern = Pattern.compile("https?://(.*?).tumblr.com(/.*)?");
|
||||
final Matcher matcher = pattern.matcher(url);
|
||||
if (matcher.matches()) {
|
||||
final String username = matcher.group(1);
|
||||
if (username.equals("www")) {
|
||||
return null;
|
||||
}
|
||||
return "http://" + username + ".tumblr.com/rss";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package org.mozilla.gecko.feeds.knownsites;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Wordpress.com
|
||||
*/
|
||||
public class KnownSiteWordpress implements KnownSite {
|
||||
@Override
|
||||
public String getURLSearchString() {
|
||||
return ".wordpress.com";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFeedFromURL(String url) {
|
||||
Pattern pattern = Pattern.compile("https?://(.*?).wordpress.com(/.*)?");
|
||||
Matcher matcher = pattern.matcher(url);
|
||||
if (matcher.matches()) {
|
||||
return "https://" + matcher.group(1) + ".wordpress.com/feed/";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds.parser;
|
||||
|
||||
import ch.boye.httpclientandroidlib.util.TextUtils;
|
||||
|
||||
public class Feed {
|
||||
private String title;
|
||||
private String websiteURL;
|
||||
private String feedURL;
|
||||
private Item lastItem;
|
||||
|
||||
public static Feed create(String title, String websiteURL, String feedURL, Item lastItem) {
|
||||
Feed feed = new Feed();
|
||||
|
||||
feed.setTitle(title);
|
||||
feed.setWebsiteURL(websiteURL);
|
||||
feed.setFeedURL(feedURL);
|
||||
feed.setLastItem(lastItem);
|
||||
|
||||
return feed;
|
||||
}
|
||||
|
||||
/* package-private */ Feed() {}
|
||||
|
||||
/* package-private */ void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/* package-private */ void setWebsiteURL(String websiteURL) {
|
||||
this.websiteURL = websiteURL;
|
||||
}
|
||||
|
||||
/* package-private */ void setFeedURL(String feedURL) {
|
||||
this.feedURL = feedURL;
|
||||
}
|
||||
|
||||
/* package-private */ void setLastItem(Item lastItem) {
|
||||
this.lastItem = lastItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this feed object sufficiently complete so that we can use it?
|
||||
*/
|
||||
/* package-private */ boolean isSufficientlyComplete() {
|
||||
return !TextUtils.isEmpty(title) &&
|
||||
lastItem != null &&
|
||||
!TextUtils.isEmpty(lastItem.getURL()) &&
|
||||
!TextUtils.isEmpty(lastItem.getTitle());
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getWebsiteURL() {
|
||||
return websiteURL;
|
||||
}
|
||||
|
||||
public String getFeedURL() {
|
||||
return feedURL;
|
||||
}
|
||||
|
||||
public Item getLastItem() {
|
||||
return lastItem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds.parser;
|
||||
|
||||
public class Item {
|
||||
private String title;
|
||||
private String url;
|
||||
private long timestamp;
|
||||
|
||||
public static Item create(String title, String url, long timestamp) {
|
||||
Item item = new Item();
|
||||
|
||||
item.setTitle(title);
|
||||
item.setURL(url);
|
||||
item.setTimestamp(timestamp);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/* package-private */ void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/* package-private */ void setURL(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/* package-private */ void setTimestamp(long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getURL() {
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of milliseconds since Jan. 1, 1970, midnight GMT.
|
||||
*/
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds.parser;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import ch.boye.httpclientandroidlib.util.TextUtils;
|
||||
|
||||
/**
|
||||
* A super simple feed parser written for implementing "content notifications". This XML Pull Parser
|
||||
* can read ATOM and RSS feeds and returns an object describing the feed and the latest entry.
|
||||
*/
|
||||
public class SimpleFeedParser {
|
||||
/**
|
||||
* Generic exception that's thrown by the parser whenever a stream cannot be parsed.
|
||||
*/
|
||||
public static class ParserException extends Exception {
|
||||
private static final long serialVersionUID = -6119538440219805603L;
|
||||
|
||||
public ParserException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ParserException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String LOGTAG = "Gecko/FeedParser";
|
||||
|
||||
private static final String TAG_RSS = "rss";
|
||||
private static final String TAG_FEED = "feed";
|
||||
private static final String TAG_RDF = "RDF";
|
||||
private static final String TAG_TITLE = "title";
|
||||
private static final String TAG_ITEM = "item";
|
||||
private static final String TAG_LINK = "link";
|
||||
private static final String TAG_ENTRY = "entry";
|
||||
private static final String TAG_PUBDATE = "pubDate";
|
||||
private static final String TAG_UPDATED = "updated";
|
||||
private static final String TAG_DATE = "date";
|
||||
private static final String TAG_SOURCE = "source";
|
||||
private static final String TAG_IMAGE = "image";
|
||||
private static final String TAG_CONTENT = "content";
|
||||
|
||||
private class ParserState {
|
||||
public Feed feed;
|
||||
public Item currentItem;
|
||||
public boolean isRSS;
|
||||
public boolean isATOM;
|
||||
public boolean inSource;
|
||||
public boolean inImage;
|
||||
public boolean inContent;
|
||||
}
|
||||
|
||||
public Feed parse(InputStream in) throws ParserException, IOException {
|
||||
final ParserState state = new ParserState();
|
||||
|
||||
try {
|
||||
final XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
|
||||
XmlPullParser parser = factory.newPullParser();
|
||||
parser.setInput(in, null);
|
||||
|
||||
int eventType = parser.getEventType();
|
||||
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
switch (eventType) {
|
||||
case XmlPullParser.START_DOCUMENT:
|
||||
handleStartDocument(state);
|
||||
break;
|
||||
|
||||
case XmlPullParser.START_TAG:
|
||||
handleStartTag(parser, state);
|
||||
break;
|
||||
|
||||
case XmlPullParser.END_TAG:
|
||||
handleEndTag(parser, state);
|
||||
break;
|
||||
}
|
||||
|
||||
eventType = parser.next();
|
||||
}
|
||||
} catch (XmlPullParserException e) {
|
||||
throw new ParserException(e);
|
||||
}
|
||||
|
||||
if (!state.feed.isSufficientlyComplete()) {
|
||||
throw new ParserException("Feed is not sufficiently complete");
|
||||
}
|
||||
|
||||
return state.feed;
|
||||
}
|
||||
|
||||
private void handleStartDocument(ParserState state) {
|
||||
state.feed = new Feed();
|
||||
}
|
||||
|
||||
private void handleStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
|
||||
switch (parser.getName()) {
|
||||
case TAG_RSS:
|
||||
state.isRSS = true;
|
||||
break;
|
||||
|
||||
case TAG_FEED:
|
||||
state.isATOM = true;
|
||||
break;
|
||||
|
||||
case TAG_RDF:
|
||||
// This is a RSS 1.0 feed
|
||||
state.isRSS = true;
|
||||
break;
|
||||
|
||||
case TAG_ITEM:
|
||||
case TAG_ENTRY:
|
||||
state.currentItem = new Item();
|
||||
break;
|
||||
|
||||
case TAG_TITLE:
|
||||
handleTitleStartTag(parser, state);
|
||||
break;
|
||||
|
||||
case TAG_LINK:
|
||||
handleLinkStartTag(parser, state);
|
||||
break;
|
||||
|
||||
case TAG_PUBDATE:
|
||||
handlePubDateStartTag(parser, state);
|
||||
break;
|
||||
|
||||
case TAG_UPDATED:
|
||||
handleUpdatedStartTag(parser, state);
|
||||
break;
|
||||
|
||||
case TAG_DATE:
|
||||
handleDateStartTag(parser, state);
|
||||
break;
|
||||
|
||||
case TAG_SOURCE:
|
||||
state.inSource = true;
|
||||
break;
|
||||
|
||||
case TAG_IMAGE:
|
||||
state.inImage = true;
|
||||
break;
|
||||
|
||||
case TAG_CONTENT:
|
||||
state.inContent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleEndTag(XmlPullParser parser, ParserState state) {
|
||||
switch (parser.getName()) {
|
||||
case TAG_ITEM:
|
||||
case TAG_ENTRY:
|
||||
handleItemOrEntryREndTag(state);
|
||||
break;
|
||||
|
||||
case TAG_SOURCE:
|
||||
state.inSource = false;
|
||||
break;
|
||||
|
||||
case TAG_IMAGE:
|
||||
state.inImage = false;
|
||||
break;
|
||||
|
||||
case TAG_CONTENT:
|
||||
state.inContent = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTitleStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
|
||||
if (state.inSource || state.inImage || state.inContent) {
|
||||
// We do not care about titles in <source>, <image> or <media> tags.
|
||||
return;
|
||||
}
|
||||
|
||||
String title = getTextUntilEndTag(parser, TAG_TITLE);
|
||||
|
||||
title = title.replaceAll("[\r\n]", " ");
|
||||
title = title.replaceAll(" +", " ");
|
||||
|
||||
if (state.currentItem != null) {
|
||||
state.currentItem.setTitle(title);
|
||||
} else {
|
||||
state.feed.setTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLinkStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
|
||||
if (state.inSource || state.inImage) {
|
||||
// We do not care about links in <source> or <image> tags.
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, String> attributes = fetchAttributes(parser);
|
||||
|
||||
if (attributes.size() > 0) {
|
||||
String rel = attributes.get("rel");
|
||||
|
||||
if (state.currentItem == null && "self".equals(rel)) {
|
||||
state.feed.setFeedURL(attributes.get("href"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (rel == null || "alternate".equals(rel)) {
|
||||
String type = attributes.get("type");
|
||||
if (type == null || type.equals("text/html")) {
|
||||
String link = attributes.get("href");
|
||||
if (TextUtils.isEmpty(link)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.currentItem != null) {
|
||||
state.currentItem.setURL(link);
|
||||
} else {
|
||||
state.feed.setWebsiteURL(link);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isRSS) {
|
||||
String link = getTextUntilEndTag(parser, TAG_LINK);
|
||||
if (TextUtils.isEmpty(link)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.currentItem != null) {
|
||||
state.currentItem.setURL(link);
|
||||
} else {
|
||||
state.feed.setWebsiteURL(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleItemOrEntryREndTag(ParserState state) {
|
||||
if (state.feed.getLastItem() == null || state.feed.getLastItem().getTimestamp() < state.currentItem.getTimestamp()) {
|
||||
// Only set this item as "last item" if we do not have an item yet or this item is newer.
|
||||
state.feed.setLastItem(state.currentItem);
|
||||
}
|
||||
|
||||
state.currentItem = null;
|
||||
}
|
||||
|
||||
private void handlePubDateStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
|
||||
if (state.currentItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String pubDate = getTextUntilEndTag(parser, TAG_PUBDATE);
|
||||
if (TextUtils.isEmpty(pubDate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// RFC-822
|
||||
SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
|
||||
|
||||
updateCurrentItemTimestamp(state, pubDate, format);
|
||||
}
|
||||
|
||||
private void handleUpdatedStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
|
||||
if (state.inSource) {
|
||||
// We do not care about stuff in <source> tags.
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.currentItem == null) {
|
||||
// We are only interested in <updated> values of feed items.
|
||||
return;
|
||||
}
|
||||
|
||||
String updated = getTextUntilEndTag(parser, TAG_UPDATED);
|
||||
if (TextUtils.isEmpty(updated)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SimpleDateFormat[] formats = new SimpleDateFormat[] {
|
||||
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US),
|
||||
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US)
|
||||
};
|
||||
|
||||
// Fix timezones SimpleDateFormat can't parse:
|
||||
// 2016-01-26T18:56:54Z -> 2016-01-26T18:56:54+0000 (Timezone: Z -> +0000)
|
||||
updated = updated.replaceFirst("Z$", "+0000");
|
||||
// 2016-01-26T18:56:54+01:00 -> 2016-01-26T18:56:54+0100 (Timezone: +01:00 -> +0100)
|
||||
updated = updated.replaceFirst("([0-9]{2})([\\+\\-])([0-9]{2}):([0-9]{2})$", "$1$2$3$4");
|
||||
|
||||
updateCurrentItemTimestamp(state, updated, formats);
|
||||
}
|
||||
|
||||
private void handleDateStartTag(XmlPullParser parser, ParserState state) throws IOException, XmlPullParserException {
|
||||
if (state.currentItem == null) {
|
||||
// We are only interested in <updated> values of feed items.
|
||||
return;
|
||||
}
|
||||
|
||||
String text = getTextUntilEndTag(parser, TAG_DATE);
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fix timezones SimpleDateFormat can't parse:
|
||||
// 2016-01-26T18:56:54+00:00 -> 2016-01-26T18:56:54+0000
|
||||
text = text.replaceFirst("([0-9]{2})([\\+\\-])([0-9]{2}):([0-9]{2})$", "$1$2$3$4");
|
||||
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US);
|
||||
|
||||
updateCurrentItemTimestamp(state, text, format);
|
||||
}
|
||||
|
||||
private void updateCurrentItemTimestamp(ParserState state, String text, SimpleDateFormat... formats) {
|
||||
for (SimpleDateFormat format : formats) {
|
||||
try {
|
||||
Date date = format.parse(text);
|
||||
state.currentItem.setTimestamp(date.getTime());
|
||||
return;
|
||||
} catch (ParseException e) {
|
||||
Log.w(LOGTAG, "Could not parse 'updated': " + text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> fetchAttributes(XmlPullParser parser) {
|
||||
Map<String, String> attributes = new HashMap<>();
|
||||
|
||||
for (int i = 0; i < parser.getAttributeCount(); i++) {
|
||||
attributes.put(parser.getAttributeName(i), parser.getAttributeValue(i));
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private String getTextUntilEndTag(XmlPullParser parser, String tag) throws IOException, XmlPullParserException {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.getEventType() == XmlPullParser.TEXT) {
|
||||
builder.append(parser.getText());
|
||||
} else if (parser.getEventType() == XmlPullParser.END_TAG && tag.equals(parser.getName())) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return builder.toString().trim();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.feeds.subscriptions;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.db.BrowserContract;
|
||||
import org.mozilla.gecko.feeds.FeedFetcher;
|
||||
import org.mozilla.gecko.feeds.parser.Item;
|
||||
|
||||
/**
|
||||
* An object describing a subscription and containing some meta data about the last time we fetched
|
||||
* the feed.
|
||||
*/
|
||||
public class FeedSubscription {
|
||||
private static final String JSON_KEY_FEED_TITLE = "feed_title";
|
||||
private static final String JSON_KEY_LAST_ITEM_TITLE = "last_item_title";
|
||||
private static final String JSON_KEY_LAST_ITEM_URL = "last_item_url";
|
||||
private static final String JSON_KEY_LAST_ITEM_TIMESTAMP = "last_item_timestamp";
|
||||
private static final String JSON_KEY_ETAG = "etag";
|
||||
private static final String JSON_KEY_LAST_MODIFIED = "last_modified";
|
||||
|
||||
private String feedUrl;
|
||||
private String feedTitle;
|
||||
private String lastItemTitle;
|
||||
private String lastItemUrl;
|
||||
private long lastItemTimestamp;
|
||||
private String etag;
|
||||
private String lastModified;
|
||||
|
||||
public static FeedSubscription create(String feedUrl, FeedFetcher.FeedResponse response) {
|
||||
FeedSubscription subscription = new FeedSubscription();
|
||||
subscription.feedUrl = feedUrl;
|
||||
|
||||
subscription.update(response);
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
public static FeedSubscription fromCursor(Cursor cursor) throws JSONException {
|
||||
final FeedSubscription subscription = new FeedSubscription();
|
||||
subscription.feedUrl = cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.URL));
|
||||
|
||||
final String value = cursor.getString(cursor.getColumnIndex(BrowserContract.UrlAnnotations.VALUE));
|
||||
subscription.fromJSON(new JSONObject(value));
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
private void fromJSON(JSONObject object) throws JSONException {
|
||||
feedTitle = object.getString(JSON_KEY_FEED_TITLE);
|
||||
lastItemTitle = object.getString(JSON_KEY_LAST_ITEM_TITLE);
|
||||
lastItemUrl = object.getString(JSON_KEY_LAST_ITEM_URL);
|
||||
lastItemTimestamp = object.getLong(JSON_KEY_LAST_ITEM_TIMESTAMP);
|
||||
etag = object.optString(JSON_KEY_ETAG);
|
||||
lastModified = object.optString(JSON_KEY_LAST_MODIFIED);
|
||||
}
|
||||
|
||||
public void update(FeedFetcher.FeedResponse response) {
|
||||
feedTitle = response.feed.getTitle();
|
||||
lastItemTitle = response.feed.getLastItem().getTitle();
|
||||
lastItemUrl = response.feed.getLastItem().getURL();
|
||||
lastItemTimestamp = response.feed.getLastItem().getTimestamp();
|
||||
etag = response.etag;
|
||||
lastModified = response.lastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guesstimate if this response is a newer representation of the feed.
|
||||
*/
|
||||
public boolean hasBeenUpdated(FeedFetcher.FeedResponse response) {
|
||||
final Item responseItem = response.feed.getLastItem();
|
||||
|
||||
if (responseItem.getTimestamp() > lastItemTimestamp) {
|
||||
// The timestamp is from a newer date so we expect that this item is a new item. But this
|
||||
// could also mean that the timestamp of an already existing item has been updated. We
|
||||
// accept that and assume that the content will have changed too in this case.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (responseItem.getTimestamp() == lastItemTimestamp && responseItem.getTimestamp() != 0) {
|
||||
// We have a timestamp that is not zero and this item has still the timestamp: It's very
|
||||
// likely that we are looking at the same item. We assume this is not new content.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!responseItem.getURL().equals(lastItemUrl)) {
|
||||
// The URL changed: It is very likely that this is a new item. At least it has been updated
|
||||
// in a way that we just treat it as new content here.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getFeedUrl() {
|
||||
return feedUrl;
|
||||
}
|
||||
|
||||
public String getFeedTitle() {
|
||||
return feedTitle;
|
||||
}
|
||||
|
||||
public String getETag() {
|
||||
return etag;
|
||||
}
|
||||
|
||||
public String getLastModified() {
|
||||
return lastModified;
|
||||
}
|
||||
|
||||
public JSONObject toJSON() throws JSONException {
|
||||
JSONObject object = new JSONObject();
|
||||
|
||||
object.put(JSON_KEY_FEED_TITLE, feedTitle);
|
||||
object.put(JSON_KEY_LAST_ITEM_TITLE, lastItemTitle);
|
||||
object.put(JSON_KEY_LAST_ITEM_URL, lastItemUrl);
|
||||
object.put(JSON_KEY_LAST_ITEM_TIMESTAMP, lastItemTimestamp);
|
||||
object.put(JSON_KEY_ETAG, etag);
|
||||
object.put(JSON_KEY_LAST_MODIFIED, lastModified);
|
||||
|
||||
return object;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue