Merge branch 'django3.0' into docker_django3.0

* django3.0: (184 commits)
  Removing log override
  Moving logging over to the newsblur log.
  Fixing search indexer background task for new celery.
  Attempting to add gunicorn errors to console/log.
  Better handling of missing subs.
  Handling missing user sub on feed delete.
  Correct encoding for strings on systems that don't have utf-8 as default encoding.
  Writing in the real urllib3 dependency for requests.
  Upgrading requests due to urllib3 incompatibility.
  Login required should use the next parameter.
  Upgrading django oauth toolkit for django 1.11.
  Handling newsletters with multiple recipients.
  Extracting image urls sometimes fails.
  Handling ajax errors in json views.
  Adding timeouts to most outbound requests.
  Sentry SDK 0.19.4.
  Removing imperfect proxy warning for every story.
  Found four more GET/POST crosses.
  Feed unread count may need a POST.
  Namespacing settings.
  ...
This commit is contained in:
Samuel Clay 2020-12-08 13:04:39 -05:00
commit 05756155b1
7261 changed files with 4918 additions and 940647 deletions

3
.gitignore vendored
View file

@ -12,6 +12,7 @@ media/iphone/build
build/
.DS_Store
**/*.perspectivev*
.vscode/*
data/
config/certificates
**/*.xcuserstate
@ -71,3 +72,5 @@ clients/android/NewsBlur/build.gradle
clients/android/NewsBlur/gradle*
clients/android/NewsBlur/settings.gradle
/docker/volumes/*
**/node_modules

View file

View file

@ -1,13 +1,12 @@
from celery import Task
from newsblur.celeryapp import app
from utils import log as logging
class EmailPopularityQuery(Task):
@app.task()
def EmailPopularityQuery(pk):
from apps.analyzer.models import MPopularityQuery
query = MPopularityQuery.objects.get(pk=pk)
logging.debug(" -> ~BB~FCRunning popularity query: ~SB%s" % query)
query.send_email()
def run(self, pk):
from apps.analyzer.models import MPopularityQuery
query = MPopularityQuery.objects.get(pk=pk)
logging.debug(" -> ~BB~FCRunning popularity query: ~SB%s" % query)
query.send_email()

View file

@ -1,21 +1,20 @@
from celery import Task
from newsblur.celeryapp import app
from django.contrib.auth.models import User
from apps.feed_import.models import UploadedOPML, OPMLImporter
from apps.reader.models import UserSubscription
from utils import log as logging
class ProcessOPML(Task):
@app.task()
def ProcessOPML(user_id):
user = User.objects.get(pk=user_id)
logging.user(user, "~FR~SBOPML upload (task) starting...")
opml = UploadedOPML.objects.filter(user_id=user_id).first()
opml_importer = OPMLImporter(opml.opml_file, user)
opml_importer.process()
def run(self, user_id):
user = User.objects.get(pk=user_id)
logging.user(user, "~FR~SBOPML upload (task) starting...")
opml = UploadedOPML.objects.filter(user_id=user_id).first()
opml_importer = OPMLImporter(opml.opml_file, user)
opml_importer.process()
feed_count = UserSubscription.objects.filter(user=user).count()
user.profile.send_upload_opml_finished_email(feed_count)
logging.user(user, "~FR~SBOPML upload (task): ~SK%s~SN~SB~FR feeds" % (feed_count))
feed_count = UserSubscription.objects.filter(user=user).count()
user.profile.send_upload_opml_finished_email(feed_count)
logging.user(user, "~FR~SBOPML upload (task): ~SK%s~SN~SB~FR feeds" % (feed_count))

View file

@ -31,9 +31,26 @@ class EmailNewsletter:
return
usf.add_folder('', 'Newsletters')
# First look for the email address
try:
feed = Feed.objects.get(feed_address=feed_address)
except Feed.MultipleObjectsReturned:
feeds = Feed.objects.filter(feed_address=feed_address)[:1]
if feeds.count():
feed = feeds[0]
except Feed.DoesNotExist:
feed = None
# If not found, check among titles user has subscribed to
if not feed:
newsletter_subs = UserSubscription.objects.filter(user=user, feed__feed_address__contains="newsletter:").only('feed')
newsletter_feed_ids = [us.feed.pk for us in newsletter_subs]
feeds = Feed.objects.filter(feed_title__iexact=sender_name, pk__in=newsletter_feed_ids)
if feeds.count():
feed = feeds[0]
# Create a new feed if it doesn't exist by sender name or email
if not feed:
feed = Feed.objects.create(feed_address=feed_address,
feed_link='http://' + sender_domain,
feed_title=sender_name,
@ -148,8 +165,8 @@ class EmailNewsletter:
return profile.user
def _feed_address(self, user, sender):
return 'newsletter:%s:%s' % (user.pk, sender)
def _feed_address(self, user, sender_email):
return 'newsletter:%s:%s' % (user.pk, sender_email)
def _split_sender(self, sender):
tokens = re.search('(.*?) <(.*?)@(.*?)>', sender)

View file

@ -1,10 +1,9 @@
from celery import Task
from newsblur.celeryapp import app
from django.contrib.auth.models import User
from apps.notifications.models import MUserFeedNotification
from utils import log as logging
class QueueNotifications(Task):
def run(self, feed_id, new_stories):
MUserFeedNotification.push_feed_notifications(feed_id, new_stories)
@app.task()
def QueueNotifications(feed_id, new_stories):
MUserFeedNotification.push_feed_notifications(feed_id, new_stories)

View file

@ -9,8 +9,6 @@ from apps.profile.models import change_password, blank_authenticate, MGiftCode,
from apps.social.models import MSocialProfile
PLANS = [
("newsblur-premium-12", mark_safe("$12 / year <span class='NB-small'>($1/month)</span>")),
("newsblur-premium-24", mark_safe("$24 / year <span class='NB-small'>($2/month)</span>")),
("newsblur-premium-36", mark_safe("$36 / year <span class='NB-small'>($3/month)</span>")),
]
@ -35,7 +33,7 @@ class StripePlusPaymentForm(StripePaymentForm):
email = forms.EmailField(widget=forms.TextInput(attrs=dict(maxlength=75)),
label='Email address',
required=False)
plan = forms.ChoiceField(required=False, widget=HorizRadioRenderer,
plan = forms.ChoiceField(required=False, widget=forms.RadioSelect,
choices=PLANS, label='Plan')

View file

@ -492,7 +492,7 @@ class Profile(models.Model):
return ipn[0].payer_email
def activate_ios_premium(self, product_identifier, transaction_identifier, amount=36):
def activate_ios_premium(self, transaction_identifier=None, amount=36):
payments = PaymentHistory.objects.filter(user=self.user,
payment_identifier=transaction_identifier,
payment_date__gte=datetime.datetime.now()-datetime.timedelta(days=3))
@ -512,7 +512,30 @@ class Profile(models.Model):
if not self.is_premium:
self.activate_premium()
logging.user(self.user, "~FG~BBNew iOS premium subscription: $%s~FW" % product_identifier)
logging.user(self.user, "~FG~BBNew iOS premium subscription: $%s~FW" % amount)
return True
def activate_android_premium(self, order_id=None, amount=36):
payments = PaymentHistory.objects.filter(user=self.user,
payment_identifier=order_id,
payment_date__gte=datetime.datetime.now()-datetime.timedelta(days=3))
if len(payments):
# Already paid
logging.user(self.user, "~FG~BBAlready paid Android premium subscription: $%s~FW" % amount)
return False
PaymentHistory.objects.create(user=self.user,
payment_date=datetime.datetime.now(),
payment_amount=amount,
payment_provider='android-subscription',
payment_identifier=order_id)
self.setup_premium_history()
if not self.is_premium:
self.activate_premium()
logging.user(self.user, "~FG~BBNew Android premium subscription: $%s~FW" % amount)
return True
@classmethod

View file

@ -1,90 +1,76 @@
import datetime
from celery import Task
from newsblur.celeryapp import app
from apps.profile.models import Profile, RNewUserQueue
from utils import log as logging
from apps.reader.models import UserSubscription, UserSubscriptionFolders
from apps.social.models import MSocialServices, MActivity, MInteraction
class EmailNewUser(Task):
def run(self, user_id):
user_profile = Profile.objects.get(user__pk=user_id)
user_profile.send_new_user_email()
@app.task(name="email-new-user")
def EmailNewUser(user_id):
user_profile = Profile.objects.get(user__pk=user_id)
user_profile.send_new_user_email()
class EmailNewPremium(Task):
def run(self, user_id):
user_profile = Profile.objects.get(user__pk=user_id)
user_profile.send_new_premium_email()
@app.task(name="email-new-premium")
def EmailNewPremium(user_id):
user_profile = Profile.objects.get(user__pk=user_id)
user_profile.send_new_premium_email()
class PremiumExpire(Task):
name = 'premium-expire'
def run(self, **kwargs):
# Get expired but grace period users
two_days_ago = datetime.datetime.now() - datetime.timedelta(days=2)
thirty_days_ago = datetime.datetime.now() - datetime.timedelta(days=30)
expired_profiles = Profile.objects.filter(is_premium=True,
premium_expire__lte=two_days_ago,
premium_expire__gt=thirty_days_ago)
logging.debug(" ---> %s users have expired premiums, emailing grace..." % expired_profiles.count())
for profile in expired_profiles:
if profile.grace_period_email_sent():
continue
profile.setup_premium_history()
if profile.premium_expire < two_days_ago:
profile.send_premium_expire_grace_period_email()
# Get fully expired users
expired_profiles = Profile.objects.filter(is_premium=True,
premium_expire__lte=thirty_days_ago)
logging.debug(" ---> %s users have expired premiums, deactivating and emailing..." % expired_profiles.count())
for profile in expired_profiles:
profile.setup_premium_history()
if profile.premium_expire < thirty_days_ago:
profile.send_premium_expire_email()
profile.deactivate_premium()
class ActivateNextNewUser(Task):
name = 'activate-next-new-user'
def run(self):
RNewUserQueue.activate_next()
class CleanupUser(Task):
name = 'cleanup-user'
def run(self, user_id):
UserSubscription.trim_user_read_stories(user_id)
UserSubscription.verify_feeds_scheduled(user_id)
Profile.count_all_feed_subscribers_for_user(user_id)
MInteraction.trim(user_id)
MActivity.trim(user_id)
UserSubscriptionFolders.add_missing_feeds_for_user(user_id)
UserSubscriptionFolders.compact_for_user(user_id)
# UserSubscription.refresh_stale_feeds(user_id)
@app.task(name="premium-expire")
def PremiumExpire(**kwargs):
# Get expired but grace period users
two_days_ago = datetime.datetime.now() - datetime.timedelta(days=2)
thirty_days_ago = datetime.datetime.now() - datetime.timedelta(days=30)
expired_profiles = Profile.objects.filter(is_premium=True,
premium_expire__lte=two_days_ago,
premium_expire__gt=thirty_days_ago)
logging.debug(" ---> %s users have expired premiums, emailing grace..." % expired_profiles.count())
for profile in expired_profiles:
if profile.grace_period_email_sent():
continue
profile.setup_premium_history()
if profile.premium_expire < two_days_ago:
profile.send_premium_expire_grace_period_email()
try:
ss = MSocialServices.objects.get(user_id=user_id)
except MSocialServices.DoesNotExist:
logging.debug(" ---> ~FRCleaning up user, can't find social_services for user_id: ~SB%s" % user_id)
return
ss.sync_twitter_photo()
# Get fully expired users
expired_profiles = Profile.objects.filter(is_premium=True,
premium_expire__lte=thirty_days_ago)
logging.debug(" ---> %s users have expired premiums, deactivating and emailing..." % expired_profiles.count())
for profile in expired_profiles:
profile.setup_premium_history()
if profile.premium_expire < thirty_days_ago:
profile.send_premium_expire_email()
profile.deactivate_premium()
class CleanSpam(Task):
name = 'clean-spam'
@app.task(name="activate-next-new-user")
def ActivateNextNewUser():
RNewUserQueue.activate_next()
def run(self, **kwargs):
logging.debug(" ---> Finding spammers...")
Profile.clear_dead_spammers(confirm=True)
@app.task(name="cleanup-user")
def CleanupUser(user_id):
UserSubscription.trim_user_read_stories(user_id)
UserSubscription.verify_feeds_scheduled(user_id)
Profile.count_all_feed_subscribers_for_user(user_id)
MInteraction.trim(user_id)
MActivity.trim(user_id)
UserSubscriptionFolders.add_missing_feeds_for_user(user_id)
UserSubscriptionFolders.compact_for_user(user_id)
# UserSubscription.refresh_stale_feeds(user_id)
try:
ss = MSocialServices.objects.get(user_id=user_id)
except MSocialServices.DoesNotExist:
logging.debug(" ---> ~FRCleaning up user, can't find social_services for user_id: ~SB%s" % user_id)
return
ss.sync_twitter_photo()
class ReimportStripeHistory(Task):
name = 'reimport-stripe-history'
@app.task(name="clean-spam")
def CleanSpam():
logging.debug(" ---> Finding spammers...")
Profile.clear_dead_spammers(confirm=True)
def run(self, **kwargs):
logging.debug(" ---> Reimporting Stripe history...")
Profile.reimport_stripe_history(limit=10, days=1)
@app.task(name="reimport-stripe-history")
def ReimportStripeHistory():
logging.debug(" ---> Reimporting Stripe history...")
Profile.reimport_stripe_history(limit=10, days=1)

View file

@ -22,6 +22,7 @@ urlpatterns = [
url(r'^never_expire_premium/?', views.never_expire_premium, name='profile-never-expire-premium'),
url(r'^upgrade_premium/?', views.upgrade_premium, name='profile-upgrade-premium'),
url(r'^save_ios_receipt/?', views.save_ios_receipt, name='save-ios-receipt'),
url(r'^save_android_receipt/?', views.save_android_receipt, name='save-android-receipt'),
url(r'^update_payment_history/?', views.update_payment_history, name='profile-update-payment-history'),
url(r'^delete_account/?', views.delete_account, name='profile-delete-account'),
url(r'^forgot_password_return/?', views.forgot_password_return, name='profile-forgot-password-return'),
@ -30,4 +31,5 @@ urlpatterns = [
url(r'^delete_all_sites/?', views.delete_all_sites, name='profile-delete-all-sites'),
url(r'^email_optout/?', views.email_optout, name='profile-email-optout'),
url(r'^ios_subscription_status/?', views.ios_subscription_status, name='profile-ios-subscription-status'),
url(r'debug/?', views.trigger_error, name='trigger-error'),
]

View file

@ -98,7 +98,8 @@ def login(request):
return render(request, 'accounts/login.html', {
'form': form,
'next': request.POST.get('next', "")})
'next': request.POST.get('next', "") or request.GET.get('next', "")
})
@csrf_exempt
def signup(request):
@ -106,16 +107,17 @@ def signup(request):
recaptcha = request.POST.get('g-recaptcha-response', None)
recaptcha_error = None
if not recaptcha:
recaptcha_error = "Please hit the \"I'm not a robot\" button."
else:
response = requests.post('https://www.google.com/recaptcha/api/siteverify', {
'secret': settings.RECAPTCHA_SECRET_KEY,
'response': recaptcha,
})
result = response.json()
if not result['success']:
recaptcha_error = "Really, please hit the \"I'm not a robot\" button."
if settings.ENFORCE_SIGNUP_CAPTCHA:
if not recaptcha:
recaptcha_error = "Please hit the \"I'm not a robot\" button."
else:
response = requests.post('https://www.google.com/recaptcha/api/siteverify', {
'secret': settings.RECAPTCHA_SECRET_KEY,
'response': recaptcha,
})
result = response.json()
if not result['success']:
recaptcha_error = "Really, please hit the \"I'm not a robot\" button."
if request.method == "POST":
form = SignupForm(data=request.POST, prefix="signup")
@ -331,7 +333,7 @@ def save_ios_receipt(request):
logging.user(request, "~BM~FBSaving iOS Receipt: %s %s" % (product_identifier, transaction_identifier))
paid = request.user.profile.activate_ios_premium(product_identifier, transaction_identifier)
paid = request.user.profile.activate_ios_premium(transaction_identifier)
if paid:
logging.user(request, "~BM~FBSending iOS Receipt email: %s %s" % (product_identifier, transaction_identifier))
subject = "iOS Premium: %s (%s)" % (request.user.profile, product_identifier)
@ -343,13 +345,32 @@ def save_ios_receipt(request):
return request.user.profile
@ajax_login_required
@json.json_view
def save_android_receipt(request):
order_id = request.POST.get('order_id')
product_id = request.POST.get('product_id')
logging.user(request, "~BM~FBSaving Android Receipt: %s %s" % (product_id, order_id))
paid = request.user.profile.activate_android_premium(order_id)
if paid:
logging.user(request, "~BM~FBSending Android Receipt email: %s %s" % (product_id, order_id))
subject = "Android Premium: %s (%s)" % (request.user.profile, product_id)
message = """User: %s (%s) -- Email: %s, product: %s, order: %s""" % (request.user.username, request.user.pk, request.user.email, product_id, order_id)
mail_admins(subject, message, fail_silently=True)
else:
logging.user(request, "~BM~FBNot sending Android Receipt email, already paid: %s %s" % (product_id, order_id))
return request.user.profile
@login_required
def stripe_form(request):
user = request.user
success_updating = False
stripe.api_key = settings.STRIPE_SECRET
plan = int(request.GET.get('plan', 2))
plan = PLANS[plan-1][0]
plan = PLANS[0][0]
renew = is_true(request.GET.get('renew', False))
error = None
@ -692,4 +713,9 @@ def ios_subscription_status(request):
return {
"code": 1
}
}
def trigger_error(request):
logging.user(request.user, "~BR~FW~SBTriggering divison by zero")
division_by_zero = 1 / 0
return HttpResponseRedirect(reverse('index'))

View file

@ -52,11 +52,11 @@ def push_callback(request, push_id):
# XXX TODO: Optimize this by removing feedparser. It just needs to find out
# the hub_url or topic has changed. ElementTree could do it.
if random.random() < 0.1:
parsed = feedparser.parse(request.raw_post_data)
parsed = feedparser.parse(request.body)
subscription.check_urls_against_pushed_data(parsed)
# Don't give fat ping, just fetch.
# subscription.feed.queue_pushed_feed_xml(request.raw_post_data)
# subscription.feed.queue_pushed_feed_xml(request.body)
if subscription.feed.active_premium_subscribers >= 1:
subscription.feed.queue_pushed_feed_xml("Fetch me", latest_push_date_delta=latest_push_date_delta)
MFetchHistory.add(feed_id=subscription.feed_id,

View file

@ -154,7 +154,8 @@ class SignupForm(forms.Form):
new_user = User(username=username)
new_user.set_password(password)
new_user.is_active = False
if not getattr(settings, 'AUTO_ENABLE_NEW_USERS', True):
new_user.is_active = False
new_user.email = email
new_user.last_login = datetime.datetime.now()
new_user.save()
@ -184,4 +185,4 @@ class FeatureForm(forms.Form):
feature = Feature(description=self.cleaned_data['description'],
date=datetime.datetime.utcnow() + datetime.timedelta(minutes=1))
feature.save()
return feature
return feature

View file

@ -771,6 +771,9 @@ class UserSubscription(models.Model):
except pymongo.errors.OperationFailure as e:
stories_db = MStory.objects(story_hash__in=unread_story_hashes)[:100]
stories = Feed.format_stories(stories_db, self.feed_id)
except pymongo.errors.OperationFailure as e:
stories_db = MStory.objects(story_hash__in=unread_story_hashes)[:25]
stories = Feed.format_stories(stories_db, self.feed_id)
unread_stories = []
for story in stories:
@ -1192,7 +1195,7 @@ class RUserStory:
redis_commands(read_story_key)
read_stories_list_key = 'lRS:%s' % user_id
r.lrem(read_stories_list_key, story_hash)
r.lrem(read_stories_list_key, 1, story_hash)
if ps and username:
ps.publish(username, 'story:unread:%s' % story_hash)
@ -1428,15 +1431,16 @@ class UserSubscriptionFolders(models.Model):
self.save()
if not multiples_found and deleted and commit_delete:
user_sub = None
try:
user_sub = UserSubscription.objects.get(user=self.user, feed=feed_id)
except Feed.DoesNotExist:
except (Feed.DoesNotExist, UserSubscription.DoesNotExist):
duplicate_feed = DuplicateFeed.objects.filter(duplicate_feed_id=feed_id)
if duplicate_feed:
try:
user_sub = UserSubscription.objects.get(user=self.user,
feed=duplicate_feed[0].feed)
except Feed.DoesNotExist:
except (Feed.DoesNotExist, UserSubscription.DoesNotExist):
return
if user_sub:
user_sub.delete()

View file

@ -1,46 +1,40 @@
import datetime
from celery import Task
from newsblur.celeryapp import app
from utils import log as logging
from django.contrib.auth.models import User
from django.conf import settings
from apps.reader.models import UserSubscription
from apps.social.models import MSocialSubscription
class FreshenHomepage(Task):
name = 'freshen-homepage'
def run(self, **kwargs):
day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
user = User.objects.get(username=settings.HOMEPAGE_USERNAME)
user.profile.last_seen_on = datetime.datetime.utcnow()
user.profile.save()
@app.task(name='freshen-homepage')
def FreshenHomepage():
day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
user = User.objects.get(username=settings.HOMEPAGE_USERNAME)
user.profile.last_seen_on = datetime.datetime.utcnow()
user.profile.save()
usersubs = UserSubscription.objects.filter(user=user)
logging.debug(" ---> %s has %s feeds, freshening..." % (user.username, usersubs.count()))
for sub in usersubs:
sub.mark_read_date = day_ago
sub.needs_unread_recalc = True
sub.save()
sub.calculate_feed_scores(silent=True)
usersubs = UserSubscription.objects.filter(user=user)
logging.debug(" ---> %s has %s feeds, freshening..." % (user.username, usersubs.count()))
for sub in usersubs:
sub.mark_read_date = day_ago
sub.needs_unread_recalc = True
sub.save()
sub.calculate_feed_scores(silent=True)
socialsubs = MSocialSubscription.objects.filter(user_id=user.pk)
logging.debug(" ---> %s has %s socialsubs, freshening..." % (user.username, socialsubs.count()))
for sub in socialsubs:
sub.mark_read_date = day_ago
sub.needs_unread_recalc = True
sub.save()
sub.calculate_feed_scores(silent=True)
socialsubs = MSocialSubscription.objects.filter(user_id=user.pk)
logging.debug(" ---> %s has %s socialsubs, freshening..." % (user.username, socialsubs.count()))
for sub in socialsubs:
sub.mark_read_date = day_ago
sub.needs_unread_recalc = True
sub.save()
sub.calculate_feed_scores(silent=True)
class CleanAnalytics(Task):
name = 'clean-analytics'
hard = 720*10
def run(self, **kwargs):
logging.debug(" ---> Cleaning analytics... %s feed fetches" % (
settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.count(),
))
day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.delete_many({
"date": {"$lt": day_ago},
})
@app.task(name='clean_analytics', time_limit=720*10)
def CleanAnalytics():
logging.debug(" ---> Cleaning analytics... %s feed fetches" % (
settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.count(),
))
day_ago = datetime.datetime.utcnow() - datetime.timedelta(days=1)
settings.MONGOANALYTICSDB.nbanalytics.feed_fetches.delete_many({
"date": {"$lt": day_ago},
})

View file

@ -559,9 +559,11 @@ def interactions_count(request):
@ajax_login_required
@json.json_view
def feed_unread_count(request):
get_post = getattr(request, request.method)
start = time.time()
user = request.user
feed_ids = request.GET.getlist('feed_id') or request.GET.getlist('feed_id[]')
feed_ids = get_post.getlist('feed_id') or get_post.getlist('feed_id[]')
force = request.GET.get('force', False)
social_feed_ids = [feed_id for feed_id in feed_ids if 'social:' in feed_id]
feed_ids = list(set(feed_ids) - set(social_feed_ids))
@ -1024,10 +1026,15 @@ def starred_story_hashes(request):
mstories = MStarredStory.objects(
user_id=user.pk
).only('story_hash', 'starred_date').order_by('-starred_date')
).only('story_hash', 'starred_date', 'starred_updated').order_by('-starred_date')
if include_timestamps:
story_hashes = [(s.story_hash, s.starred_date.strftime("%s")) for s in mstories]
story_hashes = []
for s in mstories:
date = s.starred_date
if s.starred_updated:
date = s.starred_updated
story_hashes.append((s.story_hash, date.strftime("%s")))
else:
story_hashes = [s.story_hash for s in mstories]
@ -1315,28 +1322,32 @@ def load_read_stories(request):
@json.json_view
def load_river_stories__redis(request):
limit = int(request.GET.get('limit', 12))
# get_post is request.REQUEST, since this endpoint needs to handle either
# GET or POST requests, since the parameters for this endpoint can be
# very long, at which point the max size of a GET url request is exceeded.
get_post = getattr(request, request.method)
limit = int(get_post.get('limit', 12))
start = time.time()
user = get_user(request)
message = None
feed_ids = request.GET.getlist('feeds') or request.GET.getlist('feeds[]')
feed_ids = get_post.getlist('feeds') or get_post.getlist('feeds[]')
feed_ids = [int(feed_id) for feed_id in feed_ids if feed_id]
if not feed_ids:
feed_ids = request.GET.getlist('f') or request.GET.getlist('f[]')
feed_ids = [int(feed_id) for feed_id in request.GET.getlist('f') if feed_id]
story_hashes = request.GET.getlist('h') or request.GET.getlist('h[]')
feed_ids = get_post.getlist('f') or get_post.getlist('f[]')
feed_ids = [int(feed_id) for feed_id in get_post.getlist('f') if feed_id]
story_hashes = get_post.getlist('h') or get_post.getlist('h[]')
story_hashes = story_hashes[:100]
original_feed_ids = list(feed_ids)
page = int(request.GET.get('page', 1))
order = request.GET.get('order', 'newest')
read_filter = request.GET.get('read_filter', 'unread')
query = request.GET.get('query', '').strip()
include_hidden = is_true(request.GET.get('include_hidden', False))
include_feeds = is_true(request.GET.get('include_feeds', False))
initial_dashboard = is_true(request.GET.get('initial_dashboard', False))
infrequent = is_true(request.GET.get('infrequent', False))
page = int(get_post.get('page', 1))
order = get_post.get('order', 'newest')
read_filter = get_post.get('read_filter', 'unread')
query = get_post.get('query', '').strip()
include_hidden = is_true(get_post.get('include_hidden', False))
include_feeds = is_true(get_post.get('include_feeds', False))
initial_dashboard = is_true(get_post.get('initial_dashboard', False))
infrequent = is_true(get_post.get('infrequent', False))
if infrequent:
infrequent = request.GET.get('infrequent')
infrequent = get_post.get('infrequent')
now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone)
usersubs = []
code = 1
@ -1567,9 +1578,9 @@ def complete_river(request):
@json.json_view
def unread_story_hashes__old(request):
user = get_user(request)
feed_ids = request.POST.getlist('feed_id') or request.POST.getlist('feed_id[]')
feed_ids = request.GET.getlist('feed_id') or request.GET.getlist('feed_id[]')
feed_ids = [int(feed_id) for feed_id in feed_ids if feed_id]
include_timestamps = is_true(request.POST.get('include_timestamps', False))
include_timestamps = is_true(request.GET.get('include_timestamps', False))
usersubs = {}
if not feed_ids:
@ -2661,8 +2672,8 @@ def send_story_email(request):
share_user_profile.save_sent_email()
logging.user(request, '~BMSharing story by email to %s recipient%s: ~FY~SB%s~SN~BM~FY/~SB%s' %
(len(to_addresses), '' if len(to_addresses) == 1 else 's',
logging.user(request, '~BMSharing story by email to %s recipient%s (%s): ~FY~SB%s~SN~BM~FY/~SB%s' %
(len(to_addresses), '' if len(to_addresses) == 1 else 's', to_addresses,
story['story_title'][:50], feed and feed.feed_title[:50]))
return {'code': code, 'message': message}

View file

@ -110,4 +110,4 @@ def decline_feed(request):
recommended_feed.declined_date = datetime.datetime.now()
recommended_feed.save()
return load_recommended_feed(request)
return load_recommended_feed(request)

View file

@ -215,7 +215,7 @@ class IconImporter(object):
url = self._url_from_html(content)
if not url:
try:
content = requests.get(self.cleaned_feed_link).content
content = requests.get(self.cleaned_feed_link, timeout=10).content
url = self._url_from_html(content)
except (AttributeError, SocketError, requests.ConnectionError,
requests.models.MissingSchema, requests.sessions.InvalidSchema,
@ -224,6 +224,7 @@ class IconImporter(object):
requests.models.ChunkedEncodingError,
requests.models.ContentDecodingError,
http.client.IncompleteRead,
requests.adapters.ReadTimeout,
LocationParseError, OpenSSLError, PyAsn1Error,
ValueError) as e:
logging.debug(" ---> ~SN~FRFailed~FY to fetch ~FGfeed icon~FY: %s" % e)
@ -276,14 +277,12 @@ class IconImporter(object):
@timelimit(30)
def _1(url):
headers = {
'User-Agent': 'NewsBlur Favicon Fetcher - %s subscriber%s - %s '
'(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) '
'AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 '
'Safari/534.48.3)' %
'User-Agent': 'NewsBlur Favicon Fetcher - %s subscriber%s - %s %s' %
(
self.feed.num_subscribers,
's' if self.feed.num_subscribers != 1 else '',
self.feed.permalink
self.feed.permalink,
self.feed.fake_user_agent,
),
'Connection': 'close',
'Accept': 'image/png,image/x-icon,image/*;q=0.9,*/*;q=0.8'

View file

@ -409,6 +409,10 @@ class Feed(models.Model):
def favicon_fetching(self):
return bool(not (self.favicon_not_found or self.favicon_color))
@classmethod
def get_feed_by_url(self, *args, **kwargs):
return self.get_feed_from_url(*args, **kwargs)
@classmethod
def get_feed_from_url(cls, url, create=True, aggressive=False, fetch=True, offset=0, user=None, interactive=False):
feed = None
@ -416,7 +420,10 @@ class Feed(models.Model):
original_url = url
if url and url.startswith('newsletter:'):
return cls.objects.get(feed_address=url)
try:
return cls.objects.get(feed_address=url)
except cls.MultipleObjectsReturned:
return cls.objects.filter(feed_address=url)[0]
if url and re.match('(https?://)?twitter.com/\w+/?', url):
without_rss = True
if url and re.match(r'(https?://)?(www\.)?facebook.com/\w+/?$', url):
@ -1116,20 +1123,20 @@ class Feed(models.Model):
# A known workaround is using facebook's user agent.
return 'facebookexternalhit/1.0 (+http://www.facebook.com/externalhit_uatext.php)'
ua = ('NewsBlur Feed Fetcher - %s subscriber%s - %s '
'(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/56.0.2924.87 Safari/537.36)' % (
ua = ('NewsBlur Feed Fetcher - %s subscriber%s - %s %s' % (
self.num_subscribers,
's' if self.num_subscribers != 1 else '',
self.permalink,
self.fake_user_agent,
))
return ua
@property
def fake_user_agent(self):
ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:49.0) Gecko/20100101 Firefox/49.0"
ua = ('("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
'AppleWebKit/605.1.15 (KHTML, like Gecko) '
'Version/14.0.1 Safari/605.1.15")')
return ua
@ -2796,7 +2803,17 @@ class MStory(mongo.Document):
if len(image_urls):
self.image_urls = [u for u in image_urls if u]
else:
return
max_length = MStory.image_urls.field.max_length
while len(''.join(self.image_urls)) > max_length:
if len(self.image_urls) <= 1:
self.image_urls[0] = self.image_urls[0][:max_length-1]
break
else:
self.image_urls.pop()
return self.image_urls
def fetch_original_text(self, force=False, request=None, debug=False):
@ -2833,6 +2850,7 @@ class MStarredStory(mongo.DynamicDocument):
mongoengine's inheritance model on every single row."""
user_id = mongo.IntField(unique_with=('story_guid',))
starred_date = mongo.DateTimeField()
starred_updated = mongo.DateTimeField()
story_feed_id = mongo.IntField()
story_date = mongo.DateTimeField()
story_title = mongo.StringField(max_length=1024)
@ -2879,7 +2897,8 @@ class MStarredStory(mongo.DynamicDocument):
self.story_original_content_z = zlib.compress(self.story_original_content)
self.story_original_content = None
self.story_hash = self.feed_guid_hash
self.starred_updated = datetime.datetime.now()
return super(MStarredStory, self).save(*args, **kwargs)
@classmethod
@ -3010,6 +3029,11 @@ class MStarredStoryCounts(mongo.Document):
secret_token = user.profile.secret_token
slug = self.slug if self.slug else ""
if not self.slug and self.tag:
slug = slugify(self.tag)
self.slug = slug
self.save()
return "%s/reader/starred_rss/%s/%s/%s" % (settings.NEWSBLUR_URL, self.user_id,
secret_token, slug)

View file

@ -51,13 +51,11 @@ class PageImporter(object):
@property
def headers(self):
return {
'User-Agent': 'NewsBlur Page Fetcher - %s subscriber%s - %s '
'(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) '
'AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 '
'Safari/534.48.3)' % (
'User-Agent': 'NewsBlur Page Fetcher - %s subscriber%s - %s %s' % (
self.feed.num_subscribers,
's' if self.feed.num_subscribers != 1 else '',
self.feed.permalink,
self.feed.fake_user_agent,
),
}
@ -92,11 +90,12 @@ class PageImporter(object):
data = response.read()
else:
try:
response = requests.get(feed_link, headers=self.headers)
response = requests.get(feed_link, headers=self.headers, timeout=10)
response.connection.close()
except requests.exceptions.TooManyRedirects:
response = requests.get(feed_link)
except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, TypeError) as e:
response = requests.get(feed_link, timeout=10)
except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, TypeError,
requests.adapters.ReadTimeout) as e:
logging.debug(' ***> [%-30s] Page fetch failed using requests: %s' % (self.feed.log_title[:30], e))
self.save_no_page()
return
@ -186,12 +185,18 @@ class PageImporter(object):
return
try:
response = requests.get(story_permalink, headers=self.headers)
response = requests.get(story_permalink, headers=self.headers, timeout=10)
response.connection.close()
except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, requests.exceptions.ConnectionError, requests.exceptions.TooManyRedirects) as e:
except (AttributeError, SocketError, OpenSSLError, PyAsn1Error,
requests.exceptions.ConnectionError,
requests.exceptions.TooManyRedirects,
requests.adapters.ReadTimeout) as e:
try:
response = requests.get(story_permalink)
except (AttributeError, SocketError, OpenSSLError, PyAsn1Error, requests.exceptions.ConnectionError, requests.exceptions.TooManyRedirects) as e:
response = requests.get(story_permalink, timeout=10)
except (AttributeError, SocketError, OpenSSLError, PyAsn1Error,
requests.exceptions.ConnectionError,
requests.exceptions.TooManyRedirects,
requests.adapters.ReadTimeout) as e:
logging.debug(' ***> [%-30s] Original story fetch failed using requests: %s' % (self.feed.log_title[:30], e))
return
try:
@ -293,7 +298,8 @@ class PageImporter(object):
feed_page.page_data = zlib.compress(html)
feed_page.save()
except MFeedPage.DoesNotExist:
feed_page = MFeedPage.objects.create(feed_id=self.feed.pk, page_data=html)
feed_page = MFeedPage.objects.create(feed_id=self.feed.pk,
page_data=zlib.compress(html))
return feed_page
def save_page_node(self, html):

View file

@ -3,7 +3,7 @@ import os
import shutil
import time
import redis
from celery import Task
from newsblur.celeryapp import app
from celery.exceptions import SoftTimeLimitExceeded
from utils import log as logging
from utils import s3_utils as s3
@ -13,259 +13,230 @@ from utils.mongo_raw_log_middleware import MongoDumpMiddleware
from utils.redis_raw_log_middleware import RedisDumpMiddleware
FEED_TASKING_MAX = 10000
class TaskFeeds(Task):
name = 'task-feeds'
def run(self, **kwargs):
from apps.rss_feeds.models import Feed
settings.LOG_TO_STREAM = True
now = datetime.datetime.utcnow()
start = time.time()
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
tasked_feeds_size = r.zcard('tasked_feeds')
hour_ago = now - datetime.timedelta(hours=1)
r.zremrangebyscore('fetched_feeds_last_hour', 0, int(hour_ago.strftime('%s')))
now_timestamp = int(now.strftime("%s"))
queued_feeds = r.zrangebyscore('scheduled_updates', 0, now_timestamp)
r.zremrangebyscore('scheduled_updates', 0, now_timestamp)
if not queued_feeds:
logging.debug(" ---> ~SN~FB~BMNo feeds to queue! Exiting...")
return
r.sadd('queued_feeds', *queued_feeds)
logging.debug(" ---> ~SN~FBQueuing ~SB%s~SN stale feeds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
len(queued_feeds),
r.zcard('tasked_feeds'),
r.scard('queued_feeds'),
r.zcard('scheduled_updates')))
# Regular feeds
if tasked_feeds_size < FEED_TASKING_MAX:
feeds = r.srandmember('queued_feeds', FEED_TASKING_MAX)
Feed.task_feeds(feeds, verbose=True)
active_count = len(feeds)
else:
logging.debug(" ---> ~SN~FBToo many tasked feeds. ~SB%s~SN tasked." % tasked_feeds_size)
active_count = 0
logging.debug(" ---> ~SN~FBTasking %s feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
active_count,
int((time.time() - start)),
r.zcard('tasked_feeds'),
r.scard('queued_feeds'),
r.zcard('scheduled_updates')))
class TaskBrokenFeeds(Task):
name = 'task-broken-feeds'
max_retries = 0
ignore_result = True
@app.task(name='task-feeds')
def TaskFeeds():
from apps.rss_feeds.models import Feed
settings.LOG_TO_STREAM = True
now = datetime.datetime.utcnow()
start = time.time()
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
tasked_feeds_size = r.zcard('tasked_feeds')
def run(self, **kwargs):
from apps.rss_feeds.models import Feed
settings.LOG_TO_STREAM = True
now = datetime.datetime.utcnow()
start = time.time()
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
logging.debug(" ---> ~SN~FBQueuing broken feeds...")
# Force refresh feeds
refresh_feeds = Feed.objects.filter(
active=True,
fetched_once=False,
active_subscribers__gte=1
).order_by('?')[:100]
refresh_count = refresh_feeds.count()
cp1 = time.time()
logging.debug(" ---> ~SN~FBFound %s active, unfetched broken feeds" % refresh_count)
# Mistakenly inactive feeds
hours_ago = (now - datetime.timedelta(minutes=10)).strftime('%s')
old_tasked_feeds = r.zrangebyscore('tasked_feeds', 0, hours_ago)
inactive_count = len(old_tasked_feeds)
if inactive_count:
r.zremrangebyscore('tasked_feeds', 0, hours_ago)
# r.sadd('queued_feeds', *old_tasked_feeds)
for feed_id in old_tasked_feeds:
r.zincrby('error_feeds', 1, feed_id)
feed = Feed.get_by_id(feed_id)
feed.set_next_scheduled_update()
logging.debug(" ---> ~SN~FBRe-queuing ~SB%s~SN dropped/broken feeds (~SB%s/%s~SN queued/tasked)" % (
inactive_count,
r.scard('queued_feeds'),
r.zcard('tasked_feeds')))
cp2 = time.time()
old = now - datetime.timedelta(days=1)
old_feeds = Feed.objects.filter(
next_scheduled_update__lte=old,
active_subscribers__gte=1
).order_by('?')[:500]
old_count = old_feeds.count()
cp3 = time.time()
logging.debug(" ---> ~SN~FBTasking ~SBrefresh:~FC%s~FB inactive:~FC%s~FB old:~FC%s~SN~FB broken feeds... (%.4s/%.4s/%.4s)" % (
refresh_count,
inactive_count,
old_count,
cp1 - start,
cp2 - cp1,
cp3 - cp2,
))
Feed.task_feeds(refresh_feeds, verbose=False)
Feed.task_feeds(old_feeds, verbose=False)
logging.debug(" ---> ~SN~FBTasking broken feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
int((time.time() - start)),
r.zcard('tasked_feeds'),
r.scard('queued_feeds'),
r.zcard('scheduled_updates')))
class UpdateFeeds(Task):
name = 'update-feeds'
max_retries = 0
ignore_result = True
time_limit = 10*60
soft_time_limit = 9*60
def run(self, feed_pks, **kwargs):
from apps.rss_feeds.models import Feed
from apps.statistics.models import MStatistics
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0))
compute_scores = bool(mongodb_replication_lag < 10)
profiler = DBProfilerMiddleware()
profiler_activated = profiler.process_celery()
if profiler_activated:
mongo_middleware = MongoDumpMiddleware()
mongo_middleware.process_celery(profiler)
redis_middleware = RedisDumpMiddleware()
redis_middleware.process_celery(profiler)
options = {
'quick': float(MStatistics.get('quick_fetch', 0)),
'updates_off': MStatistics.get('updates_off', False),
'compute_scores': compute_scores,
'mongodb_replication_lag': mongodb_replication_lag,
}
if not isinstance(feed_pks, list):
feed_pks = [feed_pks]
for feed_pk in feed_pks:
feed = Feed.get_by_id(feed_pk)
if not feed or feed.pk != int(feed_pk):
logging.info(" ---> ~FRRemoving feed_id %s from tasked_feeds queue, points to %s..." % (feed_pk, feed and feed.pk))
r.zrem('tasked_feeds', feed_pk)
if not feed:
continue
try:
feed.update(**options)
except SoftTimeLimitExceeded as e:
feed.save_feed_history(505, 'Timeout', e)
logging.info(" ---> [%-30s] ~BR~FWTime limit hit!~SB~FR Moving on to next feed..." % feed)
if profiler_activated: profiler.process_celery_finished()
class NewFeeds(Task):
name = 'new-feeds'
max_retries = 0
ignore_result = True
time_limit = 10*60
soft_time_limit = 9*60
def run(self, feed_pks, **kwargs):
from apps.rss_feeds.models import Feed
if not isinstance(feed_pks, list):
feed_pks = [feed_pks]
options = {}
for feed_pk in feed_pks:
feed = Feed.get_by_id(feed_pk)
if not feed: continue
feed.update(options=options)
class PushFeeds(Task):
name = 'push-feeds'
max_retries = 0
ignore_result = True
def run(self, feed_id, xml, **kwargs):
from apps.rss_feeds.models import Feed
from apps.statistics.models import MStatistics
mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0))
compute_scores = bool(mongodb_replication_lag < 60)
options = {
'feed_xml': xml,
'compute_scores': compute_scores,
'mongodb_replication_lag': mongodb_replication_lag,
}
feed = Feed.get_by_id(feed_id)
if feed:
feed.update(options=options)
class BackupMongo(Task):
name = 'backup-mongo'
max_retries = 0
ignore_result = True
hour_ago = now - datetime.timedelta(hours=1)
r.zremrangebyscore('fetched_feeds_last_hour', 0, int(hour_ago.strftime('%s')))
def run(self, **kwargs):
COLLECTIONS = "classifier_tag classifier_author classifier_feed classifier_title userstories starred_stories shared_stories category category_site sent_emails social_profile social_subscription social_services statistics feedback"
now_timestamp = int(now.strftime("%s"))
queued_feeds = r.zrangebyscore('scheduled_updates', 0, now_timestamp)
r.zremrangebyscore('scheduled_updates', 0, now_timestamp)
if not queued_feeds:
logging.debug(" ---> ~SN~FB~BMNo feeds to queue! Exiting...")
return
r.sadd('queued_feeds', *queued_feeds)
logging.debug(" ---> ~SN~FBQueuing ~SB%s~SN stale feeds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
len(queued_feeds),
r.zcard('tasked_feeds'),
r.scard('queued_feeds'),
r.zcard('scheduled_updates')))
# Regular feeds
if tasked_feeds_size < FEED_TASKING_MAX:
feeds = r.srandmember('queued_feeds', FEED_TASKING_MAX)
Feed.task_feeds(feeds, verbose=True)
active_count = len(feeds)
else:
logging.debug(" ---> ~SN~FBToo many tasked feeds. ~SB%s~SN tasked." % tasked_feeds_size)
active_count = 0
logging.debug(" ---> ~SN~FBTasking %s feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
active_count,
int((time.time() - start)),
r.zcard('tasked_feeds'),
r.scard('queued_feeds'),
r.zcard('scheduled_updates')))
date = time.strftime('%Y-%m-%d-%H-%M')
collections = COLLECTIONS.split(' ')
db_name = 'newsblur'
dir_name = 'backup_mongo_%s' % date
filename = '%s.tgz' % dir_name
@app.task(name='task-broken-feeds')
def TaskBrokenFeeds():
from apps.rss_feeds.models import Feed
settings.LOG_TO_STREAM = True
now = datetime.datetime.utcnow()
start = time.time()
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
logging.debug(" ---> ~SN~FBQueuing broken feeds...")
# Force refresh feeds
refresh_feeds = Feed.objects.filter(
active=True,
fetched_once=False,
active_subscribers__gte=1
).order_by('?')[:100]
refresh_count = refresh_feeds.count()
cp1 = time.time()
logging.debug(" ---> ~SN~FBFound %s active, unfetched broken feeds" % refresh_count)
os.mkdir(dir_name)
# Mistakenly inactive feeds
hours_ago = (now - datetime.timedelta(minutes=10)).strftime('%s')
old_tasked_feeds = r.zrangebyscore('tasked_feeds', 0, hours_ago)
inactive_count = len(old_tasked_feeds)
if inactive_count:
r.zremrangebyscore('tasked_feeds', 0, hours_ago)
# r.sadd('queued_feeds', *old_tasked_feeds)
for feed_id in old_tasked_feeds:
r.zincrby('error_feeds', 1, feed_id)
feed = Feed.get_by_id(feed_id)
feed.set_next_scheduled_update()
logging.debug(" ---> ~SN~FBRe-queuing ~SB%s~SN dropped/broken feeds (~SB%s/%s~SN queued/tasked)" % (
inactive_count,
r.scard('queued_feeds'),
r.zcard('tasked_feeds')))
cp2 = time.time()
old = now - datetime.timedelta(days=1)
old_feeds = Feed.objects.filter(
next_scheduled_update__lte=old,
active_subscribers__gte=1
).order_by('?')[:500]
old_count = old_feeds.count()
cp3 = time.time()
logging.debug(" ---> ~SN~FBTasking ~SBrefresh:~FC%s~FB inactive:~FC%s~FB old:~FC%s~SN~FB broken feeds... (%.4s/%.4s/%.4s)" % (
refresh_count,
inactive_count,
old_count,
cp1 - start,
cp2 - cp1,
cp3 - cp2,
))
Feed.task_feeds(refresh_feeds, verbose=False)
Feed.task_feeds(old_feeds, verbose=False)
logging.debug(" ---> ~SN~FBTasking broken feeds took ~SB%s~SN seconds (~SB%s~SN/~FG%s~FB~SN/%s tasked/queued/scheduled)" % (
int((time.time() - start)),
r.zcard('tasked_feeds'),
r.scard('queued_feeds'),
r.zcard('scheduled_updates')))
@app.task(name='update-feeds', time_limit=10*60, soft_time_limit=9*60, ignore_result=True)
def UpdateFeeds(feed_pks):
from apps.rss_feeds.models import Feed
from apps.statistics.models import MStatistics
r = redis.Redis(connection_pool=settings.REDIS_FEED_UPDATE_POOL)
for collection in collections:
cmd = 'mongodump --db %s --collection %s -o %s' % (db_name, collection, dir_name)
logging.debug(' ---> ~FMDumping ~SB%s~SN: %s' % (collection, cmd))
os.system(cmd)
mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0))
compute_scores = bool(mongodb_replication_lag < 10)
profiler = DBProfilerMiddleware()
profiler_activated = profiler.process_celery()
if profiler_activated:
mongo_middleware = MongoDumpMiddleware()
mongo_middleware.process_celery(profiler)
redis_middleware = RedisDumpMiddleware()
redis_middleware.process_celery(profiler)
options = {
'quick': float(MStatistics.get('quick_fetch', 0)),
'updates_off': MStatistics.get('updates_off', False),
'compute_scores': compute_scores,
'mongodb_replication_lag': mongodb_replication_lag,
}
if not isinstance(feed_pks, list):
feed_pks = [feed_pks]
for feed_pk in feed_pks:
feed = Feed.get_by_id(feed_pk)
if not feed or feed.pk != int(feed_pk):
logging.info(" ---> ~FRRemoving feed_id %s from tasked_feeds queue, points to %s..." % (feed_pk, feed and feed.pk))
r.zrem('tasked_feeds', feed_pk)
if not feed:
continue
try:
feed.update(**options)
except SoftTimeLimitExceeded as e:
feed.save_feed_history(505, 'Timeout', e)
logging.info(" ---> [%-30s] ~BR~FWTime limit hit!~SB~FR Moving on to next feed..." % feed)
if profiler_activated: profiler.process_celery_finished()
cmd = 'tar -jcf %s %s' % (filename, dir_name)
@app.task(name='new-feeds', time_limit=10*60, soft_time_limit=9*60, ignore_result=True)
def NewFeeds(feed_pks):
from apps.rss_feeds.models import Feed
if not isinstance(feed_pks, list):
feed_pks = [feed_pks]
options = {}
for feed_pk in feed_pks:
feed = Feed.get_by_id(feed_pk)
if not feed: continue
feed.update(options=options)
@app.task(name='push-feeds', ignore_result=True)
def PushFeeds(feed_id, xml):
from apps.rss_feeds.models import Feed
from apps.statistics.models import MStatistics
mongodb_replication_lag = int(MStatistics.get('mongodb_replication_lag', 0))
compute_scores = bool(mongodb_replication_lag < 60)
options = {
'feed_xml': xml,
'compute_scores': compute_scores,
'mongodb_replication_lag': mongodb_replication_lag,
}
feed = Feed.get_by_id(feed_id)
if feed:
feed.update(options=options)
@app.task(name='backup-mongo', ignore_result=True)
def BackupMongo():
COLLECTIONS = "classifier_tag classifier_author classifier_feed classifier_title userstories starred_stories shared_stories category category_site sent_emails social_profile social_subscription social_services statistics feedback"
date = time.strftime('%Y-%m-%d-%H-%M')
collections = COLLECTIONS.split(' ')
db_name = 'newsblur'
dir_name = 'backup_mongo_%s' % date
filename = '%s.tgz' % dir_name
os.mkdir(dir_name)
for collection in collections:
cmd = 'mongodump --db %s --collection %s -o %s' % (db_name, collection, dir_name)
logging.debug(' ---> ~FMDumping ~SB%s~SN: %s' % (collection, cmd))
os.system(cmd)
logging.debug(' ---> ~FRUploading ~SB~FM%s~SN~FR to S3...' % filename)
s3.save_file_in_s3(filename)
shutil.rmtree(dir_name)
os.remove(filename)
logging.debug(' ---> ~FRFinished uploading ~SB~FM%s~SN~FR to S3.' % filename)
cmd = 'tar -jcf %s %s' % (filename, dir_name)
os.system(cmd)
logging.debug(' ---> ~FRUploading ~SB~FM%s~SN~FR to S3...' % filename)
s3.save_file_in_s3(filename)
shutil.rmtree(dir_name)
os.remove(filename)
logging.debug(' ---> ~FRFinished uploading ~SB~FM%s~SN~FR to S3.' % filename)
class ScheduleImmediateFetches(Task):
@app.task()
def ScheduleImmediateFetches(feed_ids, user_id=None):
from apps.rss_feeds.models import Feed
def run(self, feed_ids, user_id=None, **kwargs):
from apps.rss_feeds.models import Feed
if not isinstance(feed_ids, list):
feed_ids = [feed_ids]
Feed.schedule_feed_fetches_immediately(feed_ids, user_id=user_id)
if not isinstance(feed_ids, list):
feed_ids = [feed_ids]
Feed.schedule_feed_fetches_immediately(feed_ids, user_id=user_id)
class SchedulePremiumSetup(Task):
@app.task()
def SchedulePremiumSetup(feed_ids):
from apps.rss_feeds.models import Feed
def run(self, feed_ids, **kwargs):
from apps.rss_feeds.models import Feed
if not isinstance(feed_ids, list):
feed_ids = [feed_ids]
Feed.setup_feeds_for_premium_subscribers(feed_ids)
class ScheduleCountTagsForUser(Task):
if not isinstance(feed_ids, list):
feed_ids = [feed_ids]
def run(self, user_id):
from apps.rss_feeds.models import MStarredStoryCounts
MStarredStoryCounts.count_for_user(user_id)
Feed.setup_feeds_for_premium_subscribers(feed_ids)
@app.task()
def ScheduleCountTagsForUser(user_id):
from apps.rss_feeds.models import MStarredStoryCounts
MStarredStoryCounts.count_for_user(user_id)

View file

@ -37,13 +37,11 @@ class TextImporter:
def headers(self):
num_subscribers = getattr(self.feed, 'num_subscribers', 0)
return {
'User-Agent': 'NewsBlur Content Fetcher - %s subscriber%s - %s '
'(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) '
'AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 '
'Safari/534.48.3)' % (
'User-Agent': 'NewsBlur Content Fetcher - %s subscriber%s - %s %s' % (
num_subscribers,
's' if num_subscribers != 1 else '',
getattr(self.feed, 'permalink', '')
getattr(self.feed, 'permalink', ''),
getattr(self.feed, 'fake_user_agent', ''),
),
}
@ -203,7 +201,7 @@ class TextImporter:
url = "https://www.newsblur.com/rss_feeds/original_text_fetcher?url=%s" % url
try:
r = requests.get(url, headers=headers)
r = requests.get(url, headers=headers, verify=False, timeout=15)
r.connection.close()
except (AttributeError, SocketError, requests.ConnectionError,
requests.models.MissingSchema, requests.sessions.InvalidSchema,
@ -211,6 +209,7 @@ class TextImporter:
requests.models.InvalidURL,
requests.models.ChunkedEncodingError,
requests.models.ContentDecodingError,
requests.adapters.ReadTimeout,
urllib3.exceptions.LocationValueError,
LocationParseError, OpenSSLError, PyAsn1Error) as e:
logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: %s" % e)

View file

@ -185,7 +185,7 @@ def load_feed_statistics_embedded(request, feed_id):
)
def assemble_statistics(user, feed_id):
timezone = user.profile.timezone
user_timezone = user.profile.timezone
stats = dict()
feed = get_object_or_404(Feed, pk=feed_id)
feed.update_all_statistics()
@ -201,7 +201,7 @@ def assemble_statistics(user, feed_id):
if feed.is_push:
try:
stats['push_expires'] = localtime_for_timezone(feed.push.lease_expires,
timezone).strftime("%Y-%m-%d %H:%M:%S")
user_timezone).strftime("%Y-%m-%d %H:%M:%S")
except PushSubscription.DoesNotExist:
stats['push_expires'] = 'Missing push'
feed.is_push = False
@ -233,7 +233,7 @@ def assemble_statistics(user, feed_id):
stats['story_count_history'] = story_count_history
# Rotate hours to match user's timezone offset
localoffset = timezone.utcoffset(datetime.datetime.utcnow())
localoffset = user_timezone.utcoffset(datetime.datetime.utcnow())
hours_offset = int(localoffset.total_seconds() / 3600)
rotated_hours = {}
for hour, value in list(stats['story_hours_history'].items()):
@ -253,7 +253,7 @@ def assemble_statistics(user, feed_id):
stats['classifier_counts'] = json.decode(feed.data.feed_classifier_counts)
# Fetch histories
fetch_history = MFetchHistory.feed(feed_id, timezone=timezone)
fetch_history = MFetchHistory.feed(feed_id, timezone=user_timezone)
stats['feed_fetch_history'] = fetch_history['feed_fetch_history']
stats['page_fetch_history'] = fetch_history['page_fetch_history']
stats['feed_push_history'] = fetch_history['push_history']
@ -515,11 +515,13 @@ def status(request):
@json.json_view
def original_text(request):
story_id = request.GET.get('story_id')
feed_id = request.GET.get('feed_id')
story_hash = request.GET.get('story_hash', None)
force = request.GET.get('force', False)
debug = request.GET.get('debug', False)
# iOS sends a POST, web sends a GET
GET_POST = getattr(request, request.method)
story_id = GET_POST.get('story_id')
feed_id = GET_POST.get('feed_id')
story_hash = GET_POST.get('story_hash', None)
force = GET_POST.get('force', False)
debug = GET_POST.get('debug', False)
if story_hash:
story, _ = MStory.find_story(story_hash=story_hash)
@ -542,7 +544,7 @@ def original_text(request):
'failed': not original_text or len(original_text) < 100,
}
@required_params('story_hash')
@required_params('story_hash', method="GET")
def original_story(request):
story_hash = request.GET.get('story_hash')
force = request.GET.get('force', False)
@ -559,7 +561,7 @@ def original_story(request):
return HttpResponse(original_page or "")
@required_params('story_hash')
@required_params('story_hash', method="GET")
@json.json_view
def story_changes(request):
story_hash = request.GET.get('story_hash', None)

View file

@ -78,7 +78,7 @@ class MUserSearch(mongo.Document):
logging.user(user, "~FCIndexing ~SB%s feeds~SN in %s chunks..." %
(total, len(feed_id_chunks)))
tasks = [IndexSubscriptionsChunkForSearch().s(feed_ids=feed_id_chunk,
tasks = [IndexSubscriptionsChunkForSearch.s(feed_ids=feed_id_chunk,
user_id=self.user_id
).set(queue='search_indexer')
for feed_id_chunk in feed_id_chunks]

View file

@ -1,26 +1,23 @@
from celery import Task
from newsblur.celeryapp import app
from utils import log as logging
class IndexSubscriptionsForSearch(Task):
@app.task()
def IndexSubscriptionsForSearch(user_id):
from apps.search.models import MUserSearch
def run(self, user_id):
from apps.search.models import MUserSearch
user_search = MUserSearch.get_user(user_id)
user_search.index_subscriptions_for_search()
user_search = MUserSearch.get_user(user_id)
user_search.index_subscriptions_for_search()
class IndexSubscriptionsChunkForSearch(Task):
@app.task()
def IndexSubscriptionsChunkForSearch(feed_ids, user_id):
logging.debug(" ---> Indexing: %s for %s" % (feed_ids, user_id))
from apps.search.models import MUserSearch
ignore_result = False
def run(self, feed_ids, user_id):
from apps.search.models import MUserSearch
user_search = MUserSearch.get_user(user_id)
user_search.index_subscriptions_chunk_for_search(feed_ids)
user_search = MUserSearch.get_user(user_id)
user_search.index_subscriptions_chunk_for_search(feed_ids)
class IndexFeedsForSearch(Task):
@app.task()
def IndexFeedsForSearch(feed_ids, user_id):
from apps.search.models import MUserSearch
def run(self, feed_ids, user_id):
from apps.search.models import MUserSearch
MUserSearch.index_feeds_for_search(feed_ids, user_id)
MUserSearch.index_feeds_for_search(feed_ids, user_id)

View file

@ -2304,6 +2304,13 @@ class MSharedStory(mongo.DynamicDocument):
image_sources = [img.get('src') for img in soup.findAll('img') if img and img.get('src')]
if len(image_sources) > 0:
self.image_urls = image_sources
max_length = MSharedStory.image_urls.field.max_length
while len(''.join(self.image_urls)) > max_length:
if len(self.image_urls) <= 1:
self.image_urls[0] = self.image_urls[0][:max_length-1]
break
else:
self.image_urls.pop()
self.save()
def calculate_image_sizes(self, force=False):
@ -2314,10 +2321,7 @@ class MSharedStory(mongo.DynamicDocument):
return self.image_sizes
headers = {
'User-Agent': 'NewsBlur Image Fetcher - %s '
'(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) '
'AppleWebKit/534.48.3 (KHTML, like Gecko) Version/5.1 '
'Safari/534.48.3)' % (
'User-Agent': 'NewsBlur Image Fetcher - %s' % (
settings.NEWSBLUR_URL
),
}
@ -2328,7 +2332,7 @@ class MSharedStory(mongo.DynamicDocument):
for image_source in self.image_urls[:10]:
if any(ignore in image_source for ignore in IGNORE_IMAGE_SOURCES):
continue
req = requests.get(image_source, headers=headers, stream=True)
req = requests.get(image_source, headers=headers, stream=True, timeout=10)
try:
datastream = BytesIO(req.content)
width, height = ImageOps.image_size(datastream)
@ -2713,7 +2717,7 @@ class MSocialServices(mongo.Document):
os.remove(filename)
else:
api.update_status(status=message)
except tweepy.TweepError as e:
except (tweepy.TweepError, requests.adapters.ReadError) as e:
user = User.objects.get(pk=self.user_id)
logging.user(user, "~FRTwitter error: ~SB%s" % e)
return
@ -2728,7 +2732,7 @@ class MSocialServices(mongo.Document):
url = shared_story.image_urls[0]
image_filename = os.path.basename(urllib.parse.urlparse(url).path)
req = requests.get(url, stream=True)
req = requests.get(url, stream=True, timeout=10)
filename = "/tmp/%s-%s" % (shared_story.story_hash, image_filename)
if req.status_code == 200:

View file

@ -1,92 +1,79 @@
from bson.objectid import ObjectId
from celery import Task
from newsblur.celeryapp import app
from apps.social.models import MSharedStory, MSocialProfile, MSocialServices, MSocialSubscription
from django.contrib.auth.models import User
from utils import log as logging
class PostToService(Task):
def run(self, shared_story_id, service):
try:
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
shared_story.post_to_service(service)
except MSharedStory.DoesNotExist:
logging.debug(" ---> Shared story not found (%s). Can't post to: %s" % (shared_story_id, service))
@app.task()
def PostToService(shared_story_id, service):
try:
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
shared_story.post_to_service(service)
except MSharedStory.DoesNotExist:
logging.debug(" ---> Shared story not found (%s). Can't post to: %s" % (shared_story_id, service))
class EmailNewFollower(Task):
def run(self, follower_user_id, followee_user_id):
user_profile = MSocialProfile.get_user(followee_user_id)
user_profile.send_email_for_new_follower(follower_user_id)
@app.task()
def EmailNewFollower(follower_user_id, followee_user_id):
user_profile = MSocialProfile.get_user(followee_user_id)
user_profile.send_email_for_new_follower(follower_user_id)
class EmailFollowRequest(Task):
def run(self, follower_user_id, followee_user_id):
user_profile = MSocialProfile.get_user(followee_user_id)
user_profile.send_email_for_follow_request(follower_user_id)
@app.task()
def EmailFollowRequest(follower_user_id, followee_user_id):
user_profile = MSocialProfile.get_user(followee_user_id)
user_profile.send_email_for_follow_request(follower_user_id)
class EmailFirstShare(Task):
def run(self, user_id):
user = User.objects.get(pk=user_id)
user.profile.send_first_share_to_blurblog_email()
@app.task()
def EmailFirstShare(user_id):
user = User.objects.get(pk=user_id)
user.profile.send_first_share_to_blurblog_email()
class EmailCommentReplies(Task):
def run(self, shared_story_id, reply_id):
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
shared_story.send_emails_for_new_reply(ObjectId(reply_id))
@app.task()
def EmailCommentReplies(shared_story_id, reply_id):
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
shared_story.send_emails_for_new_reply(ObjectId(reply_id))
class EmailStoryReshares(Task):
def run(self, shared_story_id):
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
shared_story.send_email_for_reshare()
@app.task()
def EmailStoryReshares(shared_story_id):
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
shared_story.send_email_for_reshare()
class SyncTwitterFriends(Task):
def run(self, user_id):
social_services = MSocialServices.objects.get(user_id=user_id)
social_services.sync_twitter_friends()
@app.task()
def SyncTwitterFriends(user_id):
social_services = MSocialServices.objects.get(user_id=user_id)
social_services.sync_twitter_friends()
class SyncFacebookFriends(Task):
def run(self, user_id):
social_services = MSocialServices.objects.get(user_id=user_id)
social_services.sync_facebook_friends()
@app.task()
def SyncFacebookFriends(user_id):
social_services = MSocialServices.objects.get(user_id=user_id)
social_services.sync_facebook_friends()
class SharePopularStories(Task):
name = 'share-popular-stories'
def run(self, **kwargs):
logging.debug(" ---> Sharing popular stories...")
MSharedStory.share_popular_stories(interactive=False)
@app.task(name="share-popular-stories")
def SharePopularStories():
logging.debug(" ---> Sharing popular stories...")
MSharedStory.share_popular_stories(interactive=False)
class CleanSocialSpam(Task):
name = 'clean-social-spam'
def run(self, **kwargs):
logging.debug(" ---> Finding social spammers...")
MSharedStory.count_potential_spammers(destroy=True)
@app.task(name='clean-social-spam')
def CleanSocialSpam():
logging.debug(" ---> Finding social spammers...")
MSharedStory.count_potential_spammers(destroy=True)
class UpdateRecalcForSubscription(Task):
@app.task()
def UpdateRecalcForSubscription(subscription_user_id, shared_story_id):
user = User.objects.get(pk=subscription_user_id)
socialsubs = MSocialSubscription.objects.filter(subscription_user_id=subscription_user_id)
try:
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
except MSharedStory.DoesNotExist:
return
def run(self, subscription_user_id, shared_story_id):
user = User.objects.get(pk=subscription_user_id)
socialsubs = MSocialSubscription.objects.filter(subscription_user_id=subscription_user_id)
try:
shared_story = MSharedStory.objects.get(id=ObjectId(shared_story_id))
except MSharedStory.DoesNotExist:
return
logging.debug(" ---> ~FM~SNFlipping unread recalc for ~SB%s~SN subscriptions to ~SB%s's blurblog~SN" % (
socialsubs.count(),
user.username
))
for socialsub in socialsubs:
socialsub.needs_unread_recalc = True
socialsub.save()
shared_story.publish_update_to_subscribers()
logging.debug(" ---> ~FM~SNFlipping unread recalc for ~SB%s~SN subscriptions to ~SB%s's blurblog~SN" % (
socialsubs.count(),
user.username
))
for socialsub in socialsubs:
socialsub.needs_unread_recalc = True
socialsub.save()
shared_story.publish_update_to_subscribers()

View file

@ -507,7 +507,7 @@ def load_social_page(request, user_id, username=None, **kwargs):
return render(request, template, params)
@required_params('story_id', feed_id=int)
@required_params('story_id', feed_id=int, method="GET")
def story_public_comments(request):
format = request.GET.get('format', 'json')
relative_user_id = request.GET.get('user_id', None)
@ -1175,7 +1175,7 @@ def ignore_follower(request):
return {'code': code}
@required_params('query')
@required_params('query', method="GET")
@json.json_view
def find_friends(request):
query = request.GET['query']
@ -1372,7 +1372,7 @@ def shared_stories_rss_feed(request, user_id, username):
))
return HttpResponse(rss.writeString('utf-8'), content_type='application/rss+xml')
@required_params('user_id')
@required_params('user_id', method="GET")
@json.json_view
def social_feed_trainer(request):
social_user_id = request.GET['user_id']

View file

@ -15,7 +15,7 @@ def faq(request):
return render(request, 'static/faq.xhtml')
def api(request):
filename = settings.TEMPLATE_DIRS[0] + '/static/api.yml'
filename = settings.TEMPLATES[0]['DIRS'][0] + '/static/api.yml'
api_yml_file = open(filename).read()
data = yaml.load(api_yml_file)

View file

@ -1,21 +1,17 @@
from celery import Task
from newsblur.celeryapp import app
from apps.statistics.models import MStatistics
from apps.statistics.models import MFeedback
# from utils import log as logging
from utils import log as logging
class CollectStats(Task):
name = 'collect-stats'
def run(self, **kwargs):
# logging.debug(" ---> ~FBCollecting stats...")
MStatistics.collect_statistics()
@app.task(name='collect-stats')
def CollectStats():
logging.debug(" ---> ~FBCollecting stats...")
MStatistics.collect_statistics()
class CollectFeedback(Task):
name = 'collect-feedback'
def run(self, **kwargs):
# logging.debug(" ---> ~FBCollecting feedback...")
MFeedback.collect_feedback()
@app.task(name='collect-feedback')
def CollectFeedback():
logging.debug(" ---> ~FBCollecting feedback...")
MFeedback.collect_feedback()

View file

@ -128,6 +128,13 @@
<activity
android:name=".activity.InAppBrowser" />
<activity
android:name=".activity.Premium" />
<activity
android:name=".activity.MuteConfig"
android:launchMode="singleTask"/>
<activity
android:name=".activity.SearchForFeeds" android:launchMode="singleTop" >
@ -174,7 +181,7 @@
android:exported="false">
</receiver>
<provider
android:name="android.support.v4.content.FileProvider"
android:name="androidx.core.content.FileProvider"
android:authorities="com.newsblur.fileprovider"
android:exported="false"
android:grantUriPermissions="true">

View file

@ -1,4 +1,5 @@
buildscript {
ext.kotlin_version = '1.4.10'
repositories {
mavenCentral()
maven {
@ -8,7 +9,8 @@ buildscript {
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@ -18,33 +20,40 @@ repositories {
url 'https://maven.google.com'
}
jcenter()
google()
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'checkstyle'
dependencies {
implementation 'com.android.support:support-core-utils:28.0.0'
implementation 'com.android.support:support-fragment:28.0.0'
implementation 'com.android.support:support-core-ui:28.0.0'
implementation 'com.squareup.okhttp3:okhttp:3.12.12'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.fragment:fragment-ktx:1.2.5'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.android.support:recyclerview-v7:28.0.0'
implementation 'com.android.billingclient:billing:3.0.1'
implementation 'nl.dionsegijn:konfetti:1.2.2'
implementation 'com.github.jinatonic.confetti:confetti:1.1.2'
implementation 'com.google.android.play:core:1.8.3'
}
android {
compileSdkVersion 28
compileSdkVersion 29
defaultConfig {
applicationId "com.newsblur"
minSdkVersion 21
targetSdkVersion 28
versionCode 168
versionName "10.1"
targetSdkVersion 29
versionCode 177
versionName "10.1.1"
}
compileOptions.with {
sourceCompatibility = JavaVersion.VERSION_1_7
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
viewBinding.enabled = true
android.buildFeatures.viewBinding = true
sourceSets {
main {

View file

@ -0,0 +1,2 @@
android.enableJetifier=true
android.useAndroidX=true

View file

@ -0,0 +1,20 @@
{
"version": 1,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.newsblur",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"properties": [],
"versionCode": 171,
"versionName": "171",
"enabled": true,
"outputFile": "NewsBlur-release.apk"
}
]
}

View file

@ -9,8 +9,7 @@
<item
android:top="0.5dp"
android:bottom="0.5dp">
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<shape android:shape="rectangle">
<solid android:color="@color/dark_feed_background_selected_end"/>
</shape>
</item>

View file

@ -9,8 +9,7 @@
<item
android:top="0.5dp"
android:bottom="0.5dp">
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<shape android:shape="rectangle">
<solid android:color="@color/feed_background_selected_end"/>
</shape>
</item>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#A6A6A6"
android:pathData="M19,18l2,1V3c0,-1.1 -0.9,-2 -2,-2H8.99C7.89,1 7,1.9 7,3h10c1.1,0 2,0.9 2,2v13zM15,5H5c-1.1,0 -2,0.9 -2,2v16l7,-3 7,3V7c0,-1.1 -0.9,-2 -2,-2z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#A6A6A6"
android:pathData="M8.1,13.34l2.83,-2.83L3.91,3.5c-1.56,1.56 -1.56,4.09 0,5.66l4.19,4.18zM14.88,11.53c1.53,0.71 3.68,0.21 5.27,-1.38 1.91,-1.91 2.28,-4.65 0.81,-6.12 -1.46,-1.46 -4.2,-1.1 -6.12,0.81 -1.59,1.59 -2.09,3.74 -1.38,5.27L3.7,19.87l1.41,1.41L12,14.41l6.88,6.88 1.41,-1.41L13.41,13l1.47,-1.47z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#A6A6A6"
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#A6A6A6"
android:pathData="M21,9A1,1 0,0 1,22 10A1,1 0,0 1,21 11H16.53L16.4,12.21L14.2,17.15C14,17.65 13.47,18 12.86,18H8.5C7.7,18 7,17.27 7,16.5V10C7,9.61 7.16,9.26 7.43,9L11.63,4.1L12.4,4.84C12.6,5.03 12.72,5.29 12.72,5.58L12.69,5.8L11,9H21M2,18V10H5V18H2Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#A6A6A6"
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#A6A6A6"
android:pathData="M6.18,17.82m-2.18,0a2.18,2.18 0,1 1,4.36 0a2.18,2.18 0,1 1,-4.36 0" />
<path
android:fillColor="#A6A6A6"
android:pathData="M4,4.44v2.83c7.03,0 12.73,5.7 12.73,12.73h2.83c0,-8.59 -6.97,-15.56 -15.56,-15.56zM4,10.1v2.83c3.9,0 7.07,3.17 7.07,7.07h2.83c0,-5.47 -4.43,-9.9 -9.9,-9.9z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#A6A6A6"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#A6A6A6"
android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#A6A6A6"
android:pathData="M14,17L4,17v2h10v-2zM20,9L4,9v2h16L20,9zM4,15h16v-2L4,13v2zM4,5v2h16L20,5L4,5z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>

View file

@ -27,6 +27,13 @@
android:layout_below="@id/itemlist_search_query"
/>
<include layout="@layout/row_fleuron"
android:id="@+id/footer_fleuron"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/itemlist_search_query"
android:visibility="gone"/>
<TextView
android:id="@+id/itemlist_sync_status"
android:layout_width="fill_parent"

View file

@ -167,7 +167,7 @@
/>
<!-- The scrollable and pull-able feed list. -->
<android.support.v4.widget.SwipeRefreshLayout
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -180,7 +180,7 @@
android:layout_height="match_parent"
android:tag="folderFeedListFragment" />
</android.support.v4.widget.SwipeRefreshLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!-- top_bar_border -->
<View

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<FrameLayout
android:id="@+id/container_sites_count"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="4dp"
android:paddingTop="16dp"
android:paddingEnd="8dp"
android:paddingBottom="16dp"
android:visibility="gone">
<TextView
android:id="@+id/text_reset_sites"
style="?linkText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:padding="4dp"
android:text="@string/mute_config_reset_button"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_sites"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:text="@string/mute_config_sites"
android:textColor="@color/positive"
android:textSize="12sp"
android:textStyle="bold" />
</FrameLayout>
<ExpandableListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:groupIndicator="@null" />
<TextView
android:id="@+id/text_sync_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@color/status_overlay_background"
android:gravity="center"
android:padding="2dp"
android:textColor="@color/status_overlay_text"
android:textSize="14sp"
android:visibility="gone" />
</FrameLayout>

View file

@ -0,0 +1,293 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true">
<LinearLayout
android:id="@+id/container_going_premium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/premium_title_going_premium"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:lineSpacingExtra="@dimen/extra_line_spacing"
android:text="@string/premium_subtitle"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_policies"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:lineSpacingExtra="@dimen/extra_line_spacing"
android:text="@string/premium_policies"
android:textSize="12sp" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_hand_pointing_right" />
<LinearLayout
android:id="@+id/container_sub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/text_sub_title"
style="?linkText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/premium_subscription_title"
android:textSize="14sp" />
<TextView
android:id="@+id/text_sub_price"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="4dp"
android:text="@string/premium_subscription_price"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/text_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/loading"
android:textSize="18sp" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="40dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_sync" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="40dp"
android:text="@string/premium_sync" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_folder" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="40dp"
android:text="@string/premium_read_by_folder" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_search" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="40dp"
android:text="@string/premium_search" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_bookmark" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="40dp"
android:text="@string/premium_searchable_tags" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_lock" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="40dp"
android:text="@string/premium_privacy_options" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_rss_feed" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="40dp"
android:text="@string/premium_custom_rss" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_text" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="40dp"
android:text="@string/premium_text_view" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_dining" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="40dp"
android:text="@string/premium_shiloh" />
</FrameLayout>
<ImageView
android:id="@+id/img_shiloh"
android:layout_width="104dp"
android:layout_height="104dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:scaleType="centerCrop" />
</LinearLayout>
<FrameLayout
android:id="@+id/container_gone_premium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="120dp"
android:gravity="center_horizontal"
android:text="@string/premium_title_gone_premium"
android:textSize="40sp"
android:textStyle="bold" />
<TextView
android:id="@+id/text_subscription_renewal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="320dp"
android:gravity="center_horizontal"
android:lineSpacingExtra="@dimen/extra_line_spacing"
android:textSize="18sp"
android:visibility="gone" />
</FrameLayout>
<nl.dionsegijn.konfetti.KonfettiView
android:id="@+id/konfetti"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</ScrollView>

View file

@ -10,13 +10,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<android.support.v4.view.ViewPager
<androidx.viewpager.widget.ViewPager
android:id="@+id/activity_details_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/profile_details">
<android.support.v4.view.PagerTitleStrip
<androidx.viewpager.widget.PagerTitleStrip
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
@ -24,6 +24,6 @@
android:paddingTop="4dp"
android:paddingBottom="4dp" />
</android.support.v4.view.ViewPager>
</androidx.viewpager.widget.ViewPager>
</RelativeLayout>

View file

@ -78,11 +78,11 @@
android:layout_height="1dp"
android:background="?android:attr/listDivider" />
<android.support.v7.widget.RecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_folders"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>

View file

@ -8,7 +8,7 @@
android:id="@+id/choose_folders_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?divider"
style="?android:listDivider"
android:dividerHeight="2dp" />
</RelativeLayout>

View file

@ -44,7 +44,7 @@
android:layout_height="6dp"
/>
<android.support.v7.widget.RecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/itemgridfragment_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"

View file

@ -130,7 +130,7 @@
android:id="@+id/share_bar_underline"
android:layout_width="match_parent"
android:layout_height="3dp"
style="?divider"
style="?android:divider"
android:visibility="gone" />
<TextView

View file

@ -4,7 +4,7 @@
android:layout_height="match_parent"
>
<android.support.v4.view.ViewPager
<androidx.viewpager.widget.ViewPager
android:id="@+id/reading_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"

View file

@ -2,17 +2,41 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
>
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/fleuron"
android:src="@drawable/fleuron"
android:layout_height="32dp"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
/>
android:src="@drawable/fleuron" />
<LinearLayout
android:id="@+id/container_subscribe"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/text_subscription"
style="?defaultText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/premium_subscribers_folder" />
<TextView
style="?linkText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:gravity="center_horizontal"
android:text="@string/premium_subscribers" />
</LinearLayout>
</LinearLayout>

View file

@ -12,6 +12,7 @@
android:id="@+id/check_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:clickable="false"
android:focusable="false" />
@ -20,7 +21,6 @@
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:contentDescription="@string/description_row_feed_icon"
android:scaleType="centerCrop" />
@ -39,4 +39,13 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/img_toggle"
android:layout_width="32dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:contentDescription="@string/description_row_feed_mute_toggle"
android:visibility="gone" />
</LinearLayout>

View file

@ -63,6 +63,9 @@
</group>
</menu>
</item>
<item android:id="@+id/menu_statistics"
android:title="@string/menu_statistics"
android:showAsAction="never" />
<item android:id="@+id/menu_delete_feed"
android:title="@string/menu_delete_feed"
android:showAsAction="never" />

View file

@ -21,6 +21,10 @@
android:title="@string/settings"
android:showAsAction="never" />
<item android:id="@+id/menu_mute_sites"
android:title="@string/mute_sites"
android:showAsAction="never" />
<item android:id="@+id/menu_widget"
android:title="@string/widget"
android:showAsAction="never" />
@ -43,6 +47,10 @@
android:title="@string/menu_loginas"
android:showAsAction="never"
android:visible="false"/>
<item android:id="@+id/menu_premium_account"
android:title="@string/menu_premium_account"
android:showAsAction="never" />
<item android:id="@+id/menu_logout"
android:title="@string/menu_logout"

View file

@ -54,6 +54,12 @@
</group>
</menu>
</item>
<item
android:id="@+id/menu_mute_all"
android:title="@string/menu_mute_all" />
<item
android:id="@+id/menu_mute_none"
android:title="@string/menu_mute_none" />
<item
android:id="@+id/menu_select_all"
android:title="@string/menu_select_all" />

View file

@ -23,7 +23,6 @@
<attr name="commentsHeader" format="string" />
<attr name="rowBorderTop" format="string" />
<attr name="rowBorderBottom" format="string" />
<attr name="divider" format="string" />
<attr name="profileCount" format="string" />
<attr name="profileActivityList" format="string" />
<attr name="itemHeaderDivider" format="string" />

View file

@ -2,4 +2,5 @@
<resources>
<dimen name="thumbnails_small_size">50dp</dimen>
<dimen name="thumbnails_size">90dp</dimen>
<dimen name="extra_line_spacing">4dp</dimen>
</resources>

View file

@ -16,8 +16,7 @@
<string name="login_next">Next</string>
<string name="title_feed_search">Search for feeds</string>
<string name="add_feed_message">Add \"%s\" to your feeds?</string>
<string name="loading">Loading…</string>
<string name="orig_text_loading">Fetching story text…</string>
@ -29,6 +28,7 @@
<string name="description_profile_picture">The user\'s profile picture</string>
<string name="description_row_folder_icon">folder icon</string>
<string name="description_row_feed_icon">feed icon</string>
<string name="description_row_feed_mute_toggle">feed mute toggle</string>
<string name="description_activity_icon">An icon illustrating the user\'s activity</string>
<string name="description_follow_button">Follow or unfollow a user</string>
<string name="description_comment_user">Comment user image</string>
@ -135,6 +135,7 @@
<string name="menu_send_story_full">Send story to…</string>
<string name="menu_mark_feed_as_read">Mark feed as read</string>
<string name="menu_delete_feed">Delete feed</string>
<string name="menu_statistics">Statistics</string>
<string name="menu_delete_saved_search">Delete saved search</string>
<string name="menu_unfollow">Unfollow user</string>
<string name="menu_choose_folders">Choose folders</string>
@ -184,6 +185,8 @@
<string name="menu_folder_view">Folder View</string>
<string name="menu_folder_view_nested">Nested</string>
<string name="menu_folder_view_flat">Flat</string>
<string name="menu_mute_all">Mute All</string>
<string name="menu_mute_none">Mute None</string>
<string name="menu_select_all">Select All</string>
<string name="menu_select_none">Select None</string>
<string name="menu_widget_background">Widget Background</string>
@ -213,6 +216,7 @@
<string name="menu_feedback_post">Create a feedback post</string>
<string name="menu_feedback_email">Email a bug report</string>
<string name="menu_theme_choose">Theme…</string>
<string name="menu_premium_account">Premium Account</string>
<string name="description_add_new_folder_icon">Add new folder icon</string>
@ -255,10 +259,34 @@
<string name="feed_stories_per_month">%d stories/month</string>
<string name="settings">Preferences</string>
<string name="mute_sites">Mute Sites</string>
<string name="widget">Widget</string>
<string name="title_widget_setup">Tap to setup in NewsBlur</string>
<string name="title_no_subscriptions">No active subscriptions detected</string>
<string name="title_widget_loading">Loading...</string>
<string name="premium_subscribers_folder">Reading by folder is only available to</string>
<string name="premium_subscribers_search">Search is only available to</string>
<string name="premium_subscribers">premium subscribers</string>
<string name="premium_toolbar_title">NewsBlur Premium</string>
<string name="premium_title_going_premium">Thank you so much for going premium!</string>
<string name="premium_title_gone_premium">Thank you for going premium!</string>
<string name="premium_subscription_renewal">Your premium subscription is set to\nrenew on %s</string>
<string name="premium_subscription_expiration">Your premium subscription is set\nto expire on %s</string>
<string name="premium_subscription_no_expiration">Your premium subscription is set\nto never expire. Whoa!</string>
<string name="premium_subtitle">Upgrading to a NewsBlur premium subscription gives you all of these features. Payments will be charged to your Play Store account at confirmation of purchase. Subscription renew unless auto-renew is turned off at least 24 hours before the end of the current period. Cancel at any time from Account Settings in Play Store.</string>
<string name="premium_policies"><![CDATA[See NewsBlur\'s <a href="https://newsblur.com/privacy">privacy policy</a> and <a href="https://newsblur.com/tos">terms of use</a> for details.]]></string>
<string name="premium_subscription_title">NewsBlur Premium Subscription</string>
<string name="premium_subscription_price">$35.99 per year ($3.00/month)</string>
<string name="premium_subscription_details_error">Error retrieving subscription details</string>
<string name="premium_sync">Sites updated up to 10x more often</string>
<string name="premium_read_by_folder">River of News (reading by folder)</string>
<string name="premium_search">Search sites and folders</string>
<string name="premium_searchable_tags">Save stories with searchable tags</string>
<string name="premium_privacy_options">Privacy options for your blurblog</string>
<string name="premium_custom_rss">Custom RSS feeds for folders and saves stories</string>
<string name="premium_text_view">Text view conveniently extracts the story</string>
<string name="premium_shiloh">You feed Shiloh, my poor, hungry dog, for a month</string>
<string name="settings_cat_offline">Offline</string>
<string name="settings_enable_offline">Download Stories</string>
@ -268,6 +296,12 @@
<string name="settings_keep_old_stories">Keep Stories after Reading</string>
<string name="settings_keep_old_stories_sum">Disable to reduce storage usage</string>
<string name="mute_config_title">You can follow up to 64 sites with a free standard account</string>
<string name="mute_config_message">Please mute %d sites or reset to most popular sites.</string>
<string name="mute_config_reset_button">RESET TO POPULAR SITES</string>
<string name="mute_config_upgrade">UPGRADE</string>
<string name="mute_config_sites">%1$s/%2$s</string>
<string name="menu_network_select">Download Using</string>
<string name="menu_network_select_sum">Restrict background data to chosen networks</string>
<string name="menu_network_select_opt_any">Any Network</string>
@ -364,7 +398,6 @@
<string name="settings_reading">Reading</string>
<string name="settings_immersive_enter_single_tap">Immersive Mode Via Single Tap</string>
<string name="settings_show_content_preview">Show Content Preview Text</string>
<string name="settings_show_thumbnails">Show Image Preview Thumbnails</string>
<string name="settings_thumbnails_style">Image Preview Thumbnails</string>
<string name="settings_notifications">Notifications</string>
<string name="settings_enable_notifications">Enable Notifications</string>
@ -414,6 +447,7 @@
<string name="sync_status_recounts">Catching up reading actions...</string>
<string name="sync_status_ffsync">On its way...</string>
<string name="sync_status_cleanup">Cleaning up...</string>
<string name="sync_status_starred">Sync saved stories actions…</string>
<string name="sync_status_stories">Fetching fresh stories...</string>
<string name="sync_status_unreads">Storing%sunread stories...</string>
<string name="sync_status_text">Storing text for %s stories...</string>
@ -461,12 +495,14 @@
<string name="gest_action_markunread">Mark Story Unread</string>
<string name="gest_action_save">Save Story</string>
<string name="gest_action_unsave">Unsave Story</string>
<string name="gest_action_statistics">Statistics</string>
<string-array name="ltr_gesture_action_entries">
<item>@string/gest_action_none</item>
<item>@string/gest_action_markread</item>
<item>@string/gest_action_markunread</item>
<item>@string/gest_action_save</item>
<item>@string/gest_action_unsave</item>
<item>@string/gest_action_statistics</item>
</string-array>
<string-array name="ltr_gesture_action_values">
<item>GEST_ACTION_NONE</item>
@ -474,6 +510,7 @@
<item>GEST_ACTION_MARKUNREAD</item>
<item>GEST_ACTION_SAVE</item>
<item>GEST_ACTION_UNSAVE</item>
<item>GEST_ACTION_STATISTICS</item>
</string-array>
<string name="ltr_gesture_action_value">GEST_ACTION_MARKREAD</string>
@ -484,6 +521,7 @@
<item>@string/gest_action_markunread</item>
<item>@string/gest_action_save</item>
<item>@string/gest_action_unsave</item>
<item>@string/gest_action_statistics</item>
</string-array>
<string-array name="rtl_gesture_action_values">
<item>GEST_ACTION_NONE</item>
@ -491,6 +529,7 @@
<item>GEST_ACTION_MARKUNREAD</item>
<item>GEST_ACTION_SAVE</item>
<item>GEST_ACTION_UNSAVE</item>
<item>GEST_ACTION_STATISTICS</item>
</string-array>
<string name="rtl_gesture_action_value">GEST_ACTION_MARKUNREAD</string>
@ -556,7 +595,5 @@
<string name="story_notification_channel_id">story_notification_channel</string>
<string name="story_notification_channel_name">New Stories</string>
<string name="save_widget">Save Widget</string>
<string name="select_feed">Select Feed</string>
<string name="go_to_feed">Go to feed</string>
</resources>

View file

@ -1,9 +1,9 @@
package com.newsblur.activity;
import android.content.res.Resources;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import com.newsblur.R;
import com.newsblur.domain.UserDetails;
@ -21,7 +21,7 @@ public class ActivityDetailsPagerAdapter extends FragmentPagerAdapter {
private final Profile profile;
public ActivityDetailsPagerAdapter(FragmentManager fragmentManager, Profile profile) {
super(fragmentManager);
super(fragmentManager, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.profile = profile;

View file

@ -3,7 +3,7 @@ package com.newsblur.activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import androidx.fragment.app.DialogFragment;
import android.view.View;
import com.newsblur.R;

View file

@ -2,8 +2,8 @@ package com.newsblur.activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
@ -46,6 +46,7 @@ public class AddSocial extends NbActivity {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
switch (resultCode) {
case AddTwitter.TWITTER_AUTHED:
addSocialFragment.setTwitterAuthed();

View file

@ -0,0 +1,193 @@
package com.newsblur.activity;
import android.database.Cursor;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.annotation.Nullable;
import androidx.loader.content.Loader;
import com.newsblur.R;
import com.newsblur.domain.Feed;
import com.newsblur.domain.Folder;
import com.newsblur.util.FeedOrderFilter;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.FolderViewFilter;
import com.newsblur.util.ListOrderFilter;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.WidgetBackground;
import com.newsblur.widget.WidgetUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
abstract public class FeedChooser extends NbActivity {
protected FeedChooserAdapter adapter;
protected ArrayList<Feed> feeds;
protected ArrayList<Folder> folders;
protected Map<String, Feed> feedMap = new HashMap<>();
protected ArrayList<String> folderNames = new ArrayList<>();
protected ArrayList<ArrayList<Feed>> folderChildren = new ArrayList<>();
abstract void bindLayout();
abstract void setupList();
abstract void processFeeds(Cursor cursor);
abstract void processData();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
bindLayout();
getActionBar().setDisplayHomeAsUpEnabled(true);
setupList();
loadFeeds();
loadFolders();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_feed_chooser, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
ListOrderFilter listOrderFilter = PrefsUtils.getFeedChooserListOrder(this);
if (listOrderFilter == ListOrderFilter.ASCENDING) {
menu.findItem(R.id.menu_sort_order_ascending).setChecked(true);
} else if (listOrderFilter == ListOrderFilter.DESCENDING) {
menu.findItem(R.id.menu_sort_order_descending).setChecked(true);
}
FeedOrderFilter feedOrderFilter = PrefsUtils.getFeedChooserFeedOrder(this);
if (feedOrderFilter == FeedOrderFilter.NAME) {
menu.findItem(R.id.menu_sort_by_name).setChecked(true);
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS) {
menu.findItem(R.id.menu_sort_by_subs).setChecked(true);
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH) {
menu.findItem(R.id.menu_sort_by_stories_month).setChecked(true);
} else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY) {
menu.findItem(R.id.menu_sort_by_recent_story).setChecked(true);
} else if (feedOrderFilter == FeedOrderFilter.OPENS) {
menu.findItem(R.id.menu_sort_by_number_opens).setChecked(true);
}
FolderViewFilter folderViewFilter = PrefsUtils.getFeedChooserFolderView(this);
if (folderViewFilter == FolderViewFilter.NESTED) {
menu.findItem(R.id.menu_folder_view_nested).setChecked(true);
} else if (folderViewFilter == FolderViewFilter.FLAT) {
menu.findItem(R.id.menu_folder_view_flat).setChecked(true);
}
WidgetBackground widgetBackground = PrefsUtils.getWidgetBackground(this);
if (widgetBackground == WidgetBackground.DEFAULT) {
menu.findItem(R.id.menu_widget_background_default).setChecked(true);
} else if (widgetBackground == WidgetBackground.TRANSPARENT) {
menu.findItem(R.id.menu_widget_background_transparent).setChecked(true);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
case R.id.menu_sort_order_ascending:
replaceListOrderFilter(ListOrderFilter.ASCENDING);
return true;
case R.id.menu_sort_order_descending:
replaceListOrderFilter(ListOrderFilter.DESCENDING);
return true;
case R.id.menu_sort_by_name:
replaceFeedOrderFilter(FeedOrderFilter.NAME);
return true;
case R.id.menu_sort_by_subs:
replaceFeedOrderFilter(FeedOrderFilter.SUBSCRIBERS);
return true;
case R.id.menu_sort_by_recent_story:
replaceFeedOrderFilter(FeedOrderFilter.RECENT_STORY);
return true;
case R.id.menu_sort_by_stories_month:
replaceFeedOrderFilter(FeedOrderFilter.STORIES_MONTH);
return true;
case R.id.menu_sort_by_number_opens:
replaceFeedOrderFilter(FeedOrderFilter.OPENS);
return true;
case R.id.menu_folder_view_nested:
replaceFolderView(FolderViewFilter.NESTED);
return true;
case R.id.menu_folder_view_flat:
replaceFolderView(FolderViewFilter.FLAT);
return true;
case R.id.menu_widget_background_default:
setWidgetBackground(WidgetBackground.DEFAULT);
return true;
case R.id.menu_widget_background_transparent:
setWidgetBackground(WidgetBackground.TRANSPARENT);
default:
return super.onOptionsItemSelected(item);
}
}
protected void setAdapterData() {
adapter.setData(this.folderNames, this.folderChildren, this.feeds);
}
private void replaceFeedOrderFilter(FeedOrderFilter feedOrderFilter) {
PrefsUtils.setFeedChooserFeedOrder(this, feedOrderFilter);
adapter.replaceFeedOrder(feedOrderFilter);
}
private void replaceListOrderFilter(ListOrderFilter listOrderFilter) {
PrefsUtils.setFeedChooserListOrder(this, listOrderFilter);
adapter.replaceListOrder(listOrderFilter);
}
private void replaceFolderView(FolderViewFilter folderViewFilter) {
PrefsUtils.setFeedChooserFolderView(this, folderViewFilter);
adapter.replaceFolderView(folderViewFilter);
setAdapterData();
}
private void setWidgetBackground(WidgetBackground widgetBackground) {
PrefsUtils.setWidgetBackground(this, widgetBackground);
WidgetUtils.updateWidget(this);
}
private void loadFeeds() {
Loader<Cursor> loader = FeedUtils.dbHelper.getFeedsLoader();
loader.registerListener(loader.getId(), (loader1, cursor) -> processFeeds(cursor));
loader.startLoading();
}
private void loadFolders() {
Loader<Cursor> loader = FeedUtils.dbHelper.getFoldersLoader();
loader.registerListener(loader.getId(), (loader1, cursor) -> processFolders(cursor));
loader.startLoading();
}
private void processFolders(Cursor cursor) {
ArrayList<Folder> folders = new ArrayList<>();
while (cursor != null && cursor.moveToNext()) {
Folder folder = Folder.fromCursor(cursor);
if (!folder.feedIds.isEmpty()) {
folders.add(folder);
}
}
this.folders = folders;
Collections.sort(this.folders, (o1, o2) -> Folder.compareFolderNames(o1.flatName(), o2.flatName()));
processData();
}
}

View file

@ -0,0 +1,252 @@
package com.newsblur.activity;
import android.content.Context;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.CheckBox;
import android.widget.ExpandableListView;
import android.widget.ImageView;
import android.widget.TextView;
import com.newsblur.R;
import com.newsblur.domain.Feed;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedOrderFilter;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.FolderViewFilter;
import com.newsblur.util.ListOrderFilter;
import com.newsblur.util.PrefsUtils;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;
public class FeedChooserAdapter extends BaseExpandableListAdapter {
protected final static int defaultTextSizeChild = 14;
protected final static int defaultTextSizeGroup = 13;
protected Set<String> feedIds = new HashSet<>();
protected ArrayList<String> folderNames = new ArrayList<>();
protected ArrayList<ArrayList<Feed>> folderChildren = new ArrayList<>();
protected FolderViewFilter folderViewFilter;
protected ListOrderFilter listOrderFilter;
protected FeedOrderFilter feedOrderFilter;
protected float textSize;
FeedChooserAdapter(Context context) {
folderViewFilter = PrefsUtils.getFeedChooserFolderView(context);
listOrderFilter = PrefsUtils.getFeedChooserListOrder(context);
feedOrderFilter = PrefsUtils.getFeedChooserFeedOrder(context);
textSize = PrefsUtils.getListTextSize(context);
}
@Override
public int getGroupCount() {
return folderNames.size();
}
@Override
public int getChildrenCount(int groupPosition) {
return folderChildren.get(groupPosition).size();
}
@Override
public String getGroup(int groupPosition) {
return folderNames.get(groupPosition);
}
@Override
public Feed getChild(int groupPosition, int childPosition) {
return folderChildren.get(groupPosition).get(childPosition);
}
@Override
public long getGroupId(int groupPosition) {
return folderNames.get(groupPosition).hashCode();
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return folderChildren.get(groupPosition).get(childPosition).hashCode();
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public View getGroupView(final int groupPosition, boolean isExpanded, View convertView, final ViewGroup parent) {
String folderName = folderNames.get(groupPosition);
if (folderName.equals(AppConstants.ROOT_FOLDER)) {
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_root_folder, parent, false);
} else {
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_folder, parent, false);
TextView textName = convertView.findViewById(R.id.text_folder_name);
textName.setTextSize(textSize * defaultTextSizeGroup);
textName.setText(folderName);
}
((ExpandableListView) parent).expandGroup(groupPosition);
return convertView;
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, final ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_feed, parent, false);
}
final Feed feed = folderChildren.get(groupPosition).get(childPosition);
TextView textTitle = convertView.findViewById(R.id.text_title);
TextView textDetails = convertView.findViewById(R.id.text_details);
final CheckBox checkBox = convertView.findViewById(R.id.check_box);
ImageView img = convertView.findViewById(R.id.img);
textTitle.setTextSize(textSize * defaultTextSizeChild);
textDetails.setTextSize(textSize * defaultTextSizeChild);
textTitle.setText(feed.title);
checkBox.setChecked(feedIds.contains(feed.feedId));
if (feedOrderFilter == FeedOrderFilter.NAME || feedOrderFilter == FeedOrderFilter.OPENS) {
textDetails.setText(parent.getContext().getString(R.string.feed_opens, feed.feedOpens));
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS) {
textDetails.setText(parent.getContext().getString(R.string.feed_subscribers, feed.subscribers));
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH) {
textDetails.setText(parent.getContext().getString(R.string.feed_stories_per_month, feed.storiesPerMonth));
} else {
// FeedOrderFilter.RECENT_STORY
try {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
Date dateTime = dateFormat.parse(feed.lastStoryDate);
CharSequence relativeTimeString = DateUtils.getRelativeTimeSpanString(dateTime.getTime(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
textDetails.setText(relativeTimeString);
} catch (Exception e) {
textDetails.setText(feed.lastStoryDate);
}
}
FeedUtils.iconLoader.displayImage(feed.faviconUrl, img, 0, false, img.getHeight(), true);
return convertView;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
@Override
public boolean areAllItemsEnabled() {
return super.areAllItemsEnabled();
}
protected void setData(ArrayList<String> activeFoldersNames, ArrayList<ArrayList<Feed>> activeFolderChildren, ArrayList<Feed> feeds) {
if (folderViewFilter == FolderViewFilter.NESTED) {
this.folderNames = activeFoldersNames;
this.folderChildren = activeFolderChildren;
} else {
this.folderNames = new ArrayList<>(1);
this.folderNames.add(AppConstants.ROOT_FOLDER);
this.folderChildren = new ArrayList<>();
this.folderChildren.add(feeds);
}
this.notifyDataChanged();
}
protected void replaceFeedOrder(FeedOrderFilter feedOrderFilter) {
this.feedOrderFilter = feedOrderFilter;
notifyDataChanged();
}
protected void replaceListOrder(ListOrderFilter listOrderFilter) {
this.listOrderFilter = listOrderFilter;
notifyDataChanged();
}
protected void replaceFolderView(FolderViewFilter folderViewFilter) {
this.folderViewFilter = folderViewFilter;
}
protected void notifyDataChanged() {
for (ArrayList<Feed> feedList : this.folderChildren) {
Collections.sort(feedList, getListComparator());
}
this.notifyDataSetChanged();
}
protected void setFeedIds(Set<String> feedIds) {
this.feedIds.clear();
this.feedIds.addAll(feedIds);
}
protected void replaceFeedIds(Set<String> feedIds) {
setFeedIds(feedIds);
this.notifyDataSetChanged();
}
private Comparator<Feed> getListComparator() {
return (o1, o2) -> {
if (feedOrderFilter == FeedOrderFilter.NAME && listOrderFilter == ListOrderFilter.ASCENDING) {
return o1.title.compareTo(o2.title);
} else if (feedOrderFilter == FeedOrderFilter.NAME && listOrderFilter == ListOrderFilter.DESCENDING) {
return o2.title.compareTo(o1.title);
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS && listOrderFilter == ListOrderFilter.ASCENDING) {
return Integer.valueOf(o1.subscribers).compareTo(Integer.valueOf(o2.subscribers));
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS && listOrderFilter == ListOrderFilter.DESCENDING) {
return Integer.valueOf(o2.subscribers).compareTo(Integer.valueOf(o1.subscribers));
} else if (feedOrderFilter == FeedOrderFilter.OPENS && listOrderFilter == ListOrderFilter.ASCENDING) {
return Integer.compare(o1.feedOpens, o2.feedOpens);
} else if (feedOrderFilter == FeedOrderFilter.OPENS && listOrderFilter == ListOrderFilter.DESCENDING) {
return Integer.compare(o2.feedOpens, o1.feedOpens);
} else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY && listOrderFilter == ListOrderFilter.ASCENDING) {
return compareLastStoryDateTimes(o1.lastStoryDate, o2.lastStoryDate, listOrderFilter);
} else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY && listOrderFilter == ListOrderFilter.DESCENDING) {
return compareLastStoryDateTimes(o1.lastStoryDate, o2.lastStoryDate, listOrderFilter);
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH && listOrderFilter == ListOrderFilter.ASCENDING) {
return Integer.compare(o1.storiesPerMonth, o2.storiesPerMonth);
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH && listOrderFilter == ListOrderFilter.DESCENDING) {
return Integer.compare(o2.storiesPerMonth, o1.storiesPerMonth);
}
return o1.title.compareTo(o2.title);
};
}
private int compareLastStoryDateTimes(String firstDateTime, String secondDateTime, ListOrderFilter listOrderFilter) {
try {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
// found null last story date times on feeds
if (TextUtils.isEmpty(firstDateTime)) {
firstDateTime = "2000-01-01 00:00:00";
}
if (TextUtils.isEmpty(secondDateTime)) {
secondDateTime = "2000-01-01 00:00:00";
}
Date firstDate = dateFormat.parse(firstDateTime);
Date secondDate = dateFormat.parse(secondDateTime);
if (listOrderFilter == ListOrderFilter.ASCENDING) {
return firstDate.compareTo(secondDate);
} else {
return secondDate.compareTo(firstDate);
}
} catch (ParseException e) {
e.printStackTrace();
return 0;
}
}
}

View file

@ -3,10 +3,14 @@ package com.newsblur.activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import androidx.fragment.app.DialogFragment;
import android.view.Menu;
import android.view.MenuItem;
import com.google.android.play.core.review.ReviewInfo;
import com.google.android.play.core.review.ReviewManager;
import com.google.android.play.core.review.ReviewManagerFactory;
import com.google.android.play.core.tasks.Task;
import com.newsblur.R;
import com.newsblur.domain.Feed;
import com.newsblur.fragment.DeleteFeedFragment;
@ -14,6 +18,7 @@ import com.newsblur.fragment.FeedIntelTrainerFragment;
import com.newsblur.fragment.RenameDialogFragment;
import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.UIUtils;
public class FeedItemsList extends ItemsList {
@ -22,6 +27,8 @@ public class FeedItemsList extends ItemsList {
public static final String EXTRA_FOLDER_NAME = "folderName";
private Feed feed;
private String folderName;
private ReviewManager reviewManager;
private ReviewInfo reviewInfo;
public static void startActivity(Context context, FeedSet feedSet,
Feed feed, String folderName) {
@ -36,13 +43,28 @@ public class FeedItemsList extends ItemsList {
protected void onCreate(Bundle bundle) {
feed = (Feed) getIntent().getSerializableExtra(EXTRA_FEED);
folderName = getIntent().getStringExtra(EXTRA_FOLDER_NAME);
super.onCreate(bundle);
UIUtils.setCustomActionBar(this, feed.faviconUrl, feed.title);
}
checkInAppReview();
}
public void deleteFeed() {
@Override
public void onBackPressed() {
// see checkInAppReview()
if (reviewInfo != null) {
Task<Void> flow = reviewManager.launchReviewFlow(this, reviewInfo);
flow.addOnCompleteListener(task -> {
PrefsUtils.setInAppReviewed(this);
super.onBackPressed();
});
} else {
super.onBackPressed();
}
}
public void deleteFeed() {
DialogFragment deleteFeedFragment = DeleteFeedFragment.newInstance(feed, folderName);
deleteFeedFragment.show(getSupportFragmentManager(), "dialog");
}
@ -85,6 +107,10 @@ public class FeedItemsList extends ItemsList {
// TODO: since this activity uses a feed object passed as an extra and doesn't query the DB,
// the name change won't be reflected until the activity finishes.
}
if (item.getItemId() == R.id.menu_statistics) {
FeedUtils.openStatistics(this, feed.feedId);
return true;
}
return false;
}
@ -122,4 +148,16 @@ public class FeedItemsList extends ItemsList {
String getSaveSearchFeedId() {
return "feed:" + feed.feedId;
}
private void checkInAppReview() {
if (!PrefsUtils.hasInAppReviewed(this)) {
reviewManager = ReviewManagerFactory.create(this);
Task<ReviewInfo> request = reviewManager.requestReviewFlow();
request.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
reviewInfo = task.getResult();
}
});
}
}
}

View file

@ -2,12 +2,13 @@ package com.newsblur.activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.fragment.app.FragmentActivity;
import com.newsblur.databinding.ActivityInAppBrowserBinding;
import com.newsblur.util.PrefsUtils;
@ -53,13 +54,4 @@ public class InAppBrowser extends FragmentActivity {
binding.webView.loadUrl(url);
}
@Override
public void onBackPressed() {
if (binding.webView.canGoBack()) {
binding.webView.goBack();
} else {
finish();
}
}
}
}

View file

@ -1,8 +1,8 @@
package com.newsblur.activity;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.Menu;
@ -97,7 +97,7 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
if (activeSearchQuery != null) {
binding.itemlistSearchQuery.setText(activeSearchQuery);
binding.itemlistSearchQuery.setVisibility(View.VISIBLE);
fs.setSearchQuery(activeSearchQuery);
checkSearchQuery();
}
binding.itemlistSearchQuery.setOnKeyListener(new OnKeyListener() {
@ -191,6 +191,7 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
menu.findItem(R.id.menu_instafetch_feed).setVisible(false);
menu.findItem(R.id.menu_intel).setVisible(false);
menu.findItem(R.id.menu_rename_feed).setVisible(false);
menu.findItem(R.id.menu_statistics).setVisible(false);
}
if (!fs.isInfrequent()) {
@ -349,11 +350,16 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
}
private void checkSearchQuery() {
String oldQuery = fs.getSearchQuery();
String q = binding.itemlistSearchQuery.getText().toString().trim();
if (q.length() < 1) {
updateFleuron(false);
q = null;
} else if (!PrefsUtils.getIsPremium(this)) {
updateFleuron(true);
return;
}
String oldQuery = fs.getSearchQuery();
fs.setSearchQuery(q);
if (!TextUtils.equals(q, oldQuery)) {
FeedUtils.prepareReadingSession(fs, true);
@ -364,6 +370,26 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
}
}
private void updateFleuron(boolean requiresPremium) {
FragmentTransaction transaction = getSupportFragmentManager()
.beginTransaction()
.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
if (requiresPremium) {
transaction.hide(itemSetFragment);
binding.footerFleuron.textSubscription.setText(R.string.premium_subscribers_search);
binding.footerFleuron.containerSubscribe.setVisibility(View.VISIBLE);
binding.footerFleuron.getRoot().setVisibility(View.VISIBLE);
binding.footerFleuron.containerSubscribe.setOnClickListener(view -> UIUtils.startPremiumActivity(this));
} else {
transaction.show(itemSetFragment);
binding.footerFleuron.containerSubscribe.setVisibility(View.GONE);
binding.footerFleuron.getRoot().setVisibility(View.GONE);
binding.footerFleuron.containerSubscribe.setOnClickListener(null);
}
transaction.commit();
}
@Override
public void storyOrderChanged(StoryOrder newValue) {
updateStoryOrderPreference(newValue);

View file

@ -1,9 +1,9 @@
package com.newsblur.activity;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.view.Window;
import com.newsblur.R;

View file

@ -1,9 +1,9 @@
package com.newsblur.activity;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.view.Window;
import com.newsblur.R;

View file

@ -5,9 +5,9 @@ import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.widget.SwipeRefreshLayout;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
@ -373,6 +373,14 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
} else if (item.getItemId() == R.id.menu_theme_black) {
PrefsUtils.setSelectedTheme(this, ThemeValue.BLACK);
UIUtils.restartActivity(this);
} else if (item.getItemId() == R.id.menu_premium_account) {
Intent intent = new Intent(this, Premium.class);
startActivity(intent);
return true;
} else if (item.getItemId() == R.id.menu_mute_sites) {
Intent intent = new Intent(this, MuteConfig.class);
startActivity(intent);
return true;
}
return false;
}

View file

@ -0,0 +1,221 @@
package com.newsblur.activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.database.Cursor;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.core.content.ContextCompat;
import com.newsblur.R;
import com.newsblur.databinding.ActivityMuteConfigBinding;
import com.newsblur.domain.Feed;
import com.newsblur.domain.Folder;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.UIUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class MuteConfig extends FeedChooser implements MuteConfigAdapter.FeedStateChangedListener {
private ActivityMuteConfigBinding binding;
private boolean checkedInitFeedsLimit = false;
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.findItem(R.id.menu_select_all).setVisible(false);
menu.findItem(R.id.menu_select_none).setVisible(false);
menu.findItem(R.id.menu_widget_background).setVisible(false);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_mute_all:
setFeedsState(true);
return true;
case R.id.menu_mute_none:
setFeedsState(false);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
void bindLayout() {
binding = ActivityMuteConfigBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
}
@Override
void setupList() {
adapter = new MuteConfigAdapter(this, this);
binding.listView.setAdapter(adapter);
}
@Override
void processFeeds(Cursor cursor) {
ArrayList<Feed> feeds = new ArrayList<>();
while (cursor != null && cursor.moveToNext()) {
Feed feed = Feed.fromCursor(cursor);
feeds.add(feed);
feedMap.put(feed.feedId, feed);
}
this.feeds = feeds;
processData();
}
@Override
void processData() {
if (folders != null && feeds != null) {
for (Folder folder : folders) {
ArrayList<Feed> children = new ArrayList<>();
for (String feedId : folder.feedIds) {
Feed feed = feedMap.get(feedId);
if (!children.contains(feed)) {
children.add(feed);
}
}
folderNames.add(folder.flatName());
folderChildren.add(children);
}
setAdapterData();
syncActiveFeedCount();
checkedInitFeedsLimit = true;
}
}
@Override
public void setAdapterData() {
Set<String> feedIds = new HashSet<>(this.feeds.size());
for (Feed feed : this.feeds) {
feedIds.add(feed.feedId);
}
adapter.setFeedIds(feedIds);
super.setAdapterData();
}
@Override
protected void handleUpdate(int updateType) {
super.handleUpdate(updateType);
if ((updateType & UPDATE_STATUS) != 0) {
String syncStatus = NBSyncService.getSyncStatusMessage(this, false);
if (syncStatus != null) {
binding.textSyncStatus.setText(syncStatus);
binding.textSyncStatus.setVisibility(View.VISIBLE);
} else {
binding.textSyncStatus.setVisibility(View.GONE);
}
}
}
@Override
public void onFeedStateChanged() {
syncActiveFeedCount();
}
private void syncActiveFeedCount() {
// free standard accounts can follow up to 64 sites
boolean isPremium = PrefsUtils.getIsPremium(this);
if (!isPremium && feeds != null) {
int activeSites = 0;
for (Feed feed : feeds) {
if (feed.active) {
activeSites++;
}
}
int textColorRes = activeSites > AppConstants.FREE_ACCOUNT_SITE_LIMIT ? R.color.negative : R.color.positive;
binding.textSites.setTextColor(ContextCompat.getColor(this, textColorRes));
binding.textSites.setText(String.format(getString(R.string.mute_config_sites), activeSites, AppConstants.FREE_ACCOUNT_SITE_LIMIT));
showSitesCount();
if (activeSites > AppConstants.FREE_ACCOUNT_SITE_LIMIT && !checkedInitFeedsLimit) {
showAccountFeedsLimitDialog(activeSites - AppConstants.FREE_ACCOUNT_SITE_LIMIT);
}
} else {
hideSitesCount();
}
}
private void setFeedsState(boolean isMute) {
for (Feed feed : feeds) {
feed.active = !isMute;
}
adapter.notifyDataSetChanged();
if (isMute) FeedUtils.muteFeeds(this, adapter.feedIds);
else FeedUtils.unmuteFeeds(this, adapter.feedIds);
}
private void showAccountFeedsLimitDialog(int exceededLimitCount) {
new AlertDialog.Builder(this)
.setTitle(R.string.mute_config_title)
.setMessage(String.format(getString(R.string.mute_config_message), exceededLimitCount))
.setNeutralButton(android.R.string.ok, null)
.setPositiveButton(R.string.mute_config_upgrade, (dialogInterface, i) -> openUpgradeToPremium())
.show();
}
private void showSitesCount() {
ViewGroup.LayoutParams oldLayout = binding.listView.getLayoutParams();
FrameLayout.LayoutParams newLayout = new FrameLayout.LayoutParams(oldLayout);
newLayout.topMargin = UIUtils.dp2px(this, 56);
binding.listView.setLayoutParams(newLayout);
binding.containerSitesCount.setVisibility(View.VISIBLE);
binding.textResetSites.setOnClickListener(view -> resetToPopularFeeds());
}
private void hideSitesCount() {
ViewGroup.LayoutParams oldLayout = binding.listView.getLayoutParams();
FrameLayout.LayoutParams newLayout = new FrameLayout.LayoutParams(oldLayout);
newLayout.topMargin = UIUtils.dp2px(this, 0);
binding.listView.setLayoutParams(newLayout);
binding.containerSitesCount.setVisibility(View.GONE);
binding.textResetSites.setOnClickListener(null);
}
// reset to most popular sites based on subscribers
private void resetToPopularFeeds() {
// sort descending by subscribers
Collections.sort(feeds, (f1, f2) -> {
if (TextUtils.isEmpty(f1.subscribers)) f1.subscribers = "0";
if (TextUtils.isEmpty(f2.subscribers)) f2.subscribers = "0";
return Integer.valueOf(f2.subscribers).compareTo(Integer.valueOf(f1.subscribers));
});
Set<String> activeFeedIds = new HashSet<>();
Set<String> inactiveFeedIds = new HashSet<>();
for (int index = 0; index < feeds.size(); index++) {
Feed feed = feeds.get(index);
if (index < AppConstants.FREE_ACCOUNT_SITE_LIMIT) {
activeFeedIds.add(feed.feedId);
} else {
inactiveFeedIds.add(feed.feedId);
}
}
FeedUtils.unmuteFeeds(this, activeFeedIds);
FeedUtils.muteFeeds(this, inactiveFeedIds);
finish();
}
private void openUpgradeToPremium() {
Intent intent = new Intent(this, Premium.class);
startActivity(intent);
finish();
}
}

View file

@ -0,0 +1,86 @@
package com.newsblur.activity;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import com.newsblur.R;
import com.newsblur.domain.Feed;
import com.newsblur.util.FeedUtils;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
public class MuteConfigAdapter extends FeedChooserAdapter {
private FeedStateChangedListener listener;
MuteConfigAdapter(Context context, FeedStateChangedListener listener) {
super(context);
this.listener = listener;
}
@Override
public View getGroupView(final int groupPosition, boolean isExpanded, View convertView, final ViewGroup parent) {
View groupView = super.getGroupView(groupPosition, isExpanded, convertView, parent);
groupView.setOnClickListener(v -> {
ArrayList<Feed> folderChild = MuteConfigAdapter.this.folderChildren.get(groupPosition);
boolean allAreMute = true;
for (Feed feed : folderChild) {
if (feed.active) {
allAreMute = false;
break;
}
}
Set<String> feedIds = new HashSet<>(folderChild.size());
for (Feed feed : folderChild) {
// flip active flag
feed.active = allAreMute;
feedIds.add(feed.feedId);
}
// if allAreMute initially, we need to unMute feeds
if (allAreMute) FeedUtils.unmuteFeeds(groupView.getContext(), feedIds);
else FeedUtils.muteFeeds(groupView.getContext(), feedIds);
listener.onFeedStateChanged();
notifyDataChanged();
});
return groupView;
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, final ViewGroup parent) {
View childView = super.getChildView(groupPosition, childPosition, isLastChild, convertView, parent);
final Feed feed = folderChildren.get(groupPosition).get(childPosition);
final CheckBox checkBox = childView.findViewById(R.id.check_box);
final ImageView imgToggle = childView.findViewById(R.id.img_toggle);
checkBox.setVisibility(View.GONE);
imgToggle.setVisibility(View.VISIBLE);
if (feed.active) imgToggle.setBackgroundResource(R.drawable.mute_feed_on);
else imgToggle.setBackgroundResource(R.drawable.mute_feed_off);
childView.setOnClickListener(v -> {
feed.active = !feed.active;
Set<String> feedIds = new HashSet<>(1);
feedIds.add(feed.feedId);
if (feed.active) FeedUtils.unmuteFeeds(childView.getContext(), feedIds);
else FeedUtils.muteFeeds(childView.getContext(), feedIds);
listener.onFeedStateChanged();
notifyDataChanged();
});
return childView;
}
interface FeedStateChangedListener {
void onFeedStateChanged();
}
}

View file

@ -1,11 +1,9 @@
package com.newsblur.activity;
import android.appwidget.AppWidgetManager;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import androidx.fragment.app.FragmentActivity;
import android.widget.Toast;
import com.newsblur.R;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.PrefConstants.ThemeValue;

View file

@ -0,0 +1,308 @@
package com.newsblur.activity;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Paint;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.util.Linkify;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.res.ResourcesCompat;
import com.android.billingclient.api.AcknowledgePurchaseParams;
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.newsblur.R;
import com.newsblur.databinding.ActivityPremiumBinding;
import com.newsblur.network.APIManager;
import com.newsblur.network.domain.NewsBlurResponse;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.AppConstants;
import com.newsblur.util.BetterLinkMovementMethod;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.Log;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.UIUtils;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import nl.dionsegijn.konfetti.emitters.StreamEmitter;
import nl.dionsegijn.konfetti.models.Shape;
import nl.dionsegijn.konfetti.models.Size;
public class Premium extends NbActivity {
private ActivityPremiumBinding binding;
private BillingClient billingClient;
private SkuDetails subscriptionDetails;
private Purchase purchasedSubscription;
private AcknowledgePurchaseResponseListener acknowledgePurchaseResponseListener = billingResult -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
Log.d(Premium.this.getLocalClassName(), "acknowledgePurchaseResponseListener OK");
verifyUserSubscriptionStatus();
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE) {
// Billing API version is not supported for the type requested.
Log.d(Premium.this.getLocalClassName(), "acknowledgePurchaseResponseListener BILLING_UNAVAILABLE");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE) {
// Network connection is down.
Log.d(Premium.this.getLocalClassName(), "acknowledgePurchaseResponseListener SERVICE_UNAVAILABLE");
} else {
// Handle any other error codes.
Log.d(Premium.this.getLocalClassName(), "acknowledgePurchaseResponseListener ERROR - message: " + billingResult.getDebugMessage());
}
};
private PurchasesUpdatedListener purchaseUpdateListener = (billingResult, purchases) -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && purchases != null) {
Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener OK");
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener USER_CANCELLED");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE) {
// Billing API version is not supported for the type requested.
Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener BILLING_UNAVAILABLE");
} else if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE) {
// Network connection is down.
Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener SERVICE_UNAVAILABLE");
} else {
// Handle any other error codes.
Log.d(Premium.this.getLocalClassName(), "purchaseUpdateListener ERROR - message: " + billingResult.getDebugMessage());
}
};
private BillingClientStateListener billingClientStateListener = new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
Log.d(Premium.this.getLocalClassName(), "onBillingSetupFinished OK");
retrievePlayStoreSubscriptions();
verifyUserSubscriptionStatus();
} else {
showSubscriptionDetailsError();
}
}
@Override
public void onBillingServiceDisconnected() {
Log.d(Premium.this.getLocalClassName(), "onBillingServiceDisconnected");
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
showSubscriptionDetailsError();
}
};
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
binding = ActivityPremiumBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setupUI();
setupBillingClient();
}
private void setupUI() {
UIUtils.setCustomActionBar(this, R.drawable.logo, getString(R.string.premium_toolbar_title));
// linkify before setting the string resource
BetterLinkMovementMethod.linkify(Linkify.WEB_URLS, binding.textPolicies)
.setOnLinkClickListener((textView, url) -> {
UIUtils.handleUri(Premium.this, Uri.parse(url));
return true;
});
binding.textPolicies.setText(UIUtils.fromHtml(getString(R.string.premium_policies)));
binding.textSubTitle.setPaintFlags(binding.textSubTitle.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
FeedUtils.iconLoader.displayImage(AppConstants.SHILOH_PHOTO_URL, binding.imgShiloh, 0, false);
}
private void setupBillingClient() {
billingClient = BillingClient.newBuilder(this)
.setListener(purchaseUpdateListener)
.enablePendingPurchases()
.build();
billingClient.startConnection(billingClientStateListener);
}
private void verifyUserSubscriptionStatus() {
boolean hasNewsBlurSubscription = PrefsUtils.getIsPremium(this);
Purchase playStoreSubscription = null;
Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
if (result.getPurchasesList() != null) {
for (Purchase purchase : result.getPurchasesList()) {
if (purchase.getSku().equals(AppConstants.PREMIUM_SKU)) {
playStoreSubscription = purchase;
}
}
}
if (hasNewsBlurSubscription || playStoreSubscription != null) {
binding.containerGoingPremium.setVisibility(View.GONE);
binding.containerGonePremium.setVisibility(View.VISIBLE);
long expirationTimeMs = PrefsUtils.getPremiumExpire(this);
String renewalString = null;
if (expirationTimeMs == 0) {
renewalString = getString(R.string.premium_subscription_no_expiration);
} else if (expirationTimeMs > 0) {
// date constructor expects ms
Date expirationDate = new Date(expirationTimeMs * 1000);
DateFormat dateFormat = new SimpleDateFormat("EEE, MMMM d, yyyy", Locale.getDefault());
dateFormat.setTimeZone(TimeZone.getDefault());
renewalString = getString(R.string.premium_subscription_renewal, dateFormat.format(expirationDate));
if (playStoreSubscription != null && !playStoreSubscription.isAutoRenewing()) {
renewalString = getString(R.string.premium_subscription_expiration, dateFormat.format(expirationDate));
}
}
if (!TextUtils.isEmpty(renewalString)) {
binding.textSubscriptionRenewal.setText(renewalString);
binding.textSubscriptionRenewal.setVisibility(View.VISIBLE);
}
showConfetti();
}
if (!hasNewsBlurSubscription && playStoreSubscription != null) {
purchasedSubscription = playStoreSubscription;
notifyNewsBlurOfSubscription();
}
}
private void retrievePlayStoreSubscriptions() {
List<String> skuList = new ArrayList<>(1);
// add sub SKUs from Play Store
skuList.add(AppConstants.PREMIUM_SKU);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS);
billingClient.querySkuDetailsAsync(params.build(), (billingResult, skuDetailsList) -> {
Log.d(Premium.this.getLocalClassName(), "SkuDetailsResponse");
processSkuDetailsList(skuDetailsList);
});
}
private void processSkuDetailsList(@Nullable List<SkuDetails> skuDetailsList) {
if (skuDetailsList != null) {
for (SkuDetails skuDetails : skuDetailsList) {
if (skuDetails.getSku().equals(AppConstants.PREMIUM_SKU)) {
Log.d(Premium.this.getLocalClassName(), "Sku detail: " + skuDetails.getTitle() + " | " + skuDetails.getDescription() + " | " + skuDetails.getPrice() + " | " + skuDetails.getSku());
subscriptionDetails = skuDetails;
}
}
}
if (subscriptionDetails != null) {
showSubscriptionDetails();
} else {
showSubscriptionDetailsError();
}
}
private void showSubscriptionDetailsError() {
binding.textLoading.setText(R.string.premium_subscription_details_error);
binding.textLoading.setVisibility(View.VISIBLE);
binding.containerSub.setVisibility(View.GONE);
}
private void showSubscriptionDetails() {
// handling dynamic currency and pricing for 1Y subscriptions
String currencySymbol = subscriptionDetails.getPrice().substring(0, 1);
String priceString = subscriptionDetails.getPrice().substring(1);
double price = Double.parseDouble(priceString);
StringBuilder pricingText = new StringBuilder();
pricingText.append(subscriptionDetails.getPrice());
pricingText.append(" per year (");
pricingText.append(currencySymbol);
pricingText.append(String.format(Locale.getDefault(), "%.2f", price / 12));
pricingText.append("/month)");
binding.textSubTitle.setText(subscriptionDetails.getTitle());
binding.textSubPrice.setText(pricingText);
binding.textLoading.setVisibility(View.GONE);
binding.containerSub.setVisibility(View.VISIBLE);
binding.containerSub.setOnClickListener(view -> launchBillingFlow(subscriptionDetails));
}
private void launchBillingFlow(@NonNull SkuDetails skuDetails) {
Log.d(Premium.this.getLocalClassName(), "launchBillingFlow for sku: " + skuDetails.getSku());
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
billingClient.launchBillingFlow(this, billingFlowParams);
}
private void handlePurchase(Purchase purchase) {
Log.d(Premium.this.getLocalClassName(), "handlePurchase: " + purchase.getOrderId());
purchasedSubscription = purchase;
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && purchase.isAcknowledged()) {
verifyUserSubscriptionStatus();
} else if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED && !purchase.isAcknowledged()) {
// need to acknowledge first time sub otherwise it will void
Log.d(Premium.this.getLocalClassName(), "acknowledge purchase: " + purchase.getOrderId());
AcknowledgePurchaseParams acknowledgePurchaseParams =
AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();
billingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener);
}
}
private void showConfetti() {
binding.konfetti.build()
.addColors(Color.YELLOW, Color.GREEN, Color.MAGENTA, Color.BLUE, Color.CYAN, Color.RED)
.setDirection(90)
.setFadeOutEnabled(true)
.setTimeToLive(1000L)
.addShapes(Shape.Square.INSTANCE, Shape.Circle.INSTANCE)
.addSizes(new Size(10, 5f))
.setPosition(0, binding.konfetti.getWidth() + 0f , -50f, -20f)
.streamFor(100, StreamEmitter.INDEFINITE);
}
private void notifyNewsBlurOfSubscription() {
if (purchasedSubscription != null) {
APIManager apiManager = new APIManager(this);
new AsyncTask<Void, Void, NewsBlurResponse>() {
@Override
protected NewsBlurResponse doInBackground(Void... voids) {
return apiManager.saveReceipt(purchasedSubscription.getOrderId(), purchasedSubscription.getSku());
}
@Override
protected void onPostExecute(NewsBlurResponse result) {
super.onPostExecute(result);
if (!result.isError()) {
NBSyncService.forceFeedsFolders();
triggerSync();
}
finish();
}
}.execute();
}
}
}

View file

@ -2,9 +2,9 @@ package com.newsblur.activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.ViewPager;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.viewpager.widget.ViewPager;
import android.text.TextUtils;
import android.view.MenuItem;

View file

@ -8,12 +8,12 @@ import android.content.res.Configuration;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.viewpager.widget.ViewPager;
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
@ -178,7 +178,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
transaction.commit();
}
getSupportLoaderManager().initLoader(0, null, this);
LoaderManager.getInstance(this).initLoader(0, null, this);
}
@Override
@ -436,7 +436,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
private void updateCursor() {
synchronized (STORIES_MUTEX) {
try {
getSupportLoaderManager().restartLoader(0, null, this);
LoaderManager.getInstance(this).restartLoader(0, null, this);
} catch (IllegalStateException ise) {
; // our heavy use of async can race loader calls, which it will gripe about, but this
// is only a refresh call, so dropping a refresh during creation is perfectly fine.

View file

@ -1,9 +1,9 @@
package com.newsblur.activity;
import android.support.v4.app.FragmentActivity;
import androidx.fragment.app.FragmentActivity;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.newsblur.R;
import com.newsblur.fragment.RegisterProgressFragment;

View file

@ -8,9 +8,10 @@ import java.util.Set;
import android.app.SearchManager;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import androidx.fragment.app.DialogFragment;
import androidx.loader.app.LoaderManager;
import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.Loader;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@ -55,7 +56,7 @@ public class SearchForFeeds extends NbActivity implements LoaderCallbacks<Search
resultsList.setEmptyView(emptyView);
resultsList.setOnItemClickListener(this);
resultsList.setItemsCanFocus(false);
searchLoader = getSupportLoaderManager().initLoader(0, new Bundle(), this);
searchLoader = LoaderManager.getInstance(this).initLoader(0, new Bundle(), this);
onSearchRequested();
}
@ -70,6 +71,7 @@ public class SearchForFeeds extends NbActivity implements LoaderCallbacks<Search
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
handleIntent(intent);
}
@ -83,7 +85,7 @@ public class SearchForFeeds extends NbActivity implements LoaderCallbacks<Search
Bundle bundle = new Bundle();
bundle.putString(SearchAsyncTaskLoader.SEARCH_TERM, query);
searchLoader = getSupportLoaderManager().restartLoader(0, bundle, this);
searchLoader = LoaderManager.getInstance(this).restartLoader(0, bundle, this);
searchLoader.forceLoad();
}

View file

@ -1,6 +1,6 @@
package com.newsblur.activity;
import android.support.v4.app.FragmentActivity;
import androidx.fragment.app.FragmentActivity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.MenuItem;

View file

@ -1,10 +1,6 @@
package com.newsblur.activity;
import android.database.Cursor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.Loader;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@ -14,43 +10,18 @@ import com.newsblur.R;
import com.newsblur.databinding.ActivityWidgetConfigBinding;
import com.newsblur.domain.Feed;
import com.newsblur.domain.Folder;
import com.newsblur.util.FeedOrderFilter;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.FolderViewFilter;
import com.newsblur.util.ListOrderFilter;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.WidgetBackground;
import com.newsblur.widget.WidgetUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class WidgetConfig extends NbActivity {
public class WidgetConfig extends FeedChooser {
private WidgetConfigAdapter adapter;
private ArrayList<Feed> feeds;
private ArrayList<Folder> folders;
private Map<String, Feed> feedMap = new HashMap<>();
private ArrayList<String> folderNames = new ArrayList<>();
private ArrayList<ArrayList<Feed>> folderChildren = new ArrayList<>();
private ActivityWidgetConfigBinding binding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityWidgetConfigBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
getActionBar().setDisplayHomeAsUpEnabled(true);
setupList();
loadFeeds();
loadFolders();
}
@Override
protected void onPause() {
super.onPause();
@ -61,127 +32,46 @@ public class WidgetConfig extends NbActivity {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_widget, menu);
inflater.inflate(R.menu.menu_feed_chooser, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
ListOrderFilter listOrderFilter = PrefsUtils.getWidgetConfigListOrder(this);
if (listOrderFilter == ListOrderFilter.ASCENDING) {
menu.findItem(R.id.menu_sort_order_ascending).setChecked(true);
} else if (listOrderFilter == ListOrderFilter.DESCENDING) {
menu.findItem(R.id.menu_sort_order_descending).setChecked(true);
}
FeedOrderFilter feedOrderFilter = PrefsUtils.getWidgetConfigFeedOrder(this);
if (feedOrderFilter == FeedOrderFilter.NAME) {
menu.findItem(R.id.menu_sort_by_name).setChecked(true);
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS) {
menu.findItem(R.id.menu_sort_by_subs).setChecked(true);
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH) {
menu.findItem(R.id.menu_sort_by_stories_month).setChecked(true);
} else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY) {
menu.findItem(R.id.menu_sort_by_recent_story).setChecked(true);
} else if (feedOrderFilter == FeedOrderFilter.OPENS) {
menu.findItem(R.id.menu_sort_by_number_opens).setChecked(true);
}
FolderViewFilter folderViewFilter = PrefsUtils.getWidgetConfigFolderView(this);
if (folderViewFilter == FolderViewFilter.NESTED) {
menu.findItem(R.id.menu_folder_view_nested).setChecked(true);
} else if (folderViewFilter == FolderViewFilter.FLAT) {
menu.findItem(R.id.menu_folder_view_flat).setChecked(true);
}
WidgetBackground widgetBackground = PrefsUtils.getWidgetBackground(this);
if (widgetBackground == WidgetBackground.DEFAULT) {
menu.findItem(R.id.menu_widget_background_default).setChecked(true);
} else if (widgetBackground == WidgetBackground.TRANSPARENT) {
menu.findItem(R.id.menu_widget_background_transparent).setChecked(true);
}
menu.findItem(R.id.menu_mute_all).setVisible(false);
menu.findItem(R.id.menu_mute_none).setVisible(false);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
case R.id.menu_sort_order_ascending:
replaceListOrderFilter(ListOrderFilter.ASCENDING);
return true;
case R.id.menu_sort_order_descending:
replaceListOrderFilter(ListOrderFilter.DESCENDING);
return true;
case R.id.menu_sort_by_name:
replaceFeedOrderFilter(FeedOrderFilter.NAME);
return true;
case R.id.menu_sort_by_subs:
replaceFeedOrderFilter(FeedOrderFilter.SUBSCRIBERS);
return true;
case R.id.menu_sort_by_recent_story:
replaceFeedOrderFilter(FeedOrderFilter.RECENT_STORY);
return true;
case R.id.menu_sort_by_stories_month:
replaceFeedOrderFilter(FeedOrderFilter.STORIES_MONTH);
return true;
case R.id.menu_sort_by_number_opens:
replaceFeedOrderFilter(FeedOrderFilter.OPENS);
return true;
case R.id.menu_folder_view_nested:
replaceFolderView(FolderViewFilter.NESTED);
return true;
case R.id.menu_folder_view_flat:
replaceFolderView(FolderViewFilter.FLAT);
return true;
case R.id.menu_select_all:
selectAllFeeds();
return true;
case R.id.menu_select_none:
replaceWidgetFeedIds(Collections.<String>emptySet());
return true;
case R.id.menu_widget_background_default:
setWidgetBackground(WidgetBackground.DEFAULT);
return true;
case R.id.menu_widget_background_transparent:
setWidgetBackground(WidgetBackground.TRANSPARENT);
replaceWidgetFeedIds(Collections.emptySet());
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void setupList() {
@Override
void bindLayout() {
binding = ActivityWidgetConfigBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
}
@Override
void setupList() {
adapter = new WidgetConfigAdapter(this);
binding.listView.setAdapter(adapter);
}
private void loadFeeds() {
Loader<Cursor> loader = FeedUtils.dbHelper.getFeedsLoader();
loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener<Cursor>() {
@Override
public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor cursor) {
processFeeds(cursor);
}
});
loader.startLoading();
}
private void loadFolders() {
Loader<Cursor> loader = FeedUtils.dbHelper.getFoldersLoader();
loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener<Cursor>() {
@Override
public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor cursor) {
processFolders(cursor);
}
});
loader.startLoading();
}
private void processFeeds(Cursor cursor) {
@Override
void processFeeds(Cursor cursor) {
ArrayList<Feed> feeds = new ArrayList<>();
while (cursor != null && cursor.moveToNext()) {
Feed feed = Feed.fromCursor(cursor);
@ -194,25 +84,25 @@ public class WidgetConfig extends NbActivity {
processData();
}
private void processFolders(Cursor cursor) {
ArrayList<Folder> folders = new ArrayList<>();
while (cursor != null && cursor.moveToNext()) {
Folder folder = Folder.fromCursor(cursor);
if (!folder.feedIds.isEmpty()) {
folders.add(folder);
@Override
public void setAdapterData() {
Set<String> feedIds = PrefsUtils.getWidgetFeedIds(this);
// by default select all feeds
if (feedIds == null) {
feedIds = new HashSet<>(this.feeds.size());
for (Feed feed : this.feeds) {
feedIds.add(feed.feedId);
}
}
this.folders = folders;
Collections.sort(this.folders, new Comparator<Folder>() {
@Override
public int compare(Folder o1, Folder o2) {
return Folder.compareFolderNames(o1.flatName(), o2.flatName());
}
});
processData();
adapter.setFeedIds(feedIds);
super.setAdapterData();
binding.listView.setVisibility(this.feeds.isEmpty() ? View.GONE : View.VISIBLE);
binding.textNoSubscriptions.setVisibility(this.feeds.isEmpty() ? View.VISIBLE : View.GONE);
}
private void processData() {
@Override
void processData() {
if (folders != null && feeds != null) {
for (Folder folder : folders) {
ArrayList<Feed> activeFeeds = new ArrayList<>();
@ -226,7 +116,6 @@ public class WidgetConfig extends NbActivity {
folderChildren.add(activeFeeds);
}
setSelectedFeeds();
setAdapterData();
}
}
@ -243,44 +132,4 @@ public class WidgetConfig extends NbActivity {
PrefsUtils.setWidgetFeedIds(this, feedIds);
adapter.replaceFeedIds(feedIds);
}
private void replaceFeedOrderFilter(FeedOrderFilter feedOrderFilter) {
PrefsUtils.setWidgetConfigFeedOrder(this, feedOrderFilter);
adapter.replaceFeedOrder(feedOrderFilter);
}
private void replaceListOrderFilter(ListOrderFilter listOrderFilter) {
PrefsUtils.setWidgetConfigListOrder(this, listOrderFilter);
adapter.replaceListOrder(listOrderFilter);
}
private void replaceFolderView(FolderViewFilter folderViewFilter) {
PrefsUtils.setWidgetConfigFolderView(this, folderViewFilter);
adapter.replaceFolderView(folderViewFilter);
setAdapterData();
}
private void setWidgetBackground(WidgetBackground widgetBackground) {
PrefsUtils.setWidgetBackground(this, widgetBackground);
WidgetUtils.updateWidget(this);
}
private void setSelectedFeeds() {
Set<String> feedIds = PrefsUtils.getWidgetFeedIds(this);
// by default select all feeds
if (feedIds == null) {
feedIds = new HashSet<>(this.feeds.size());
for (Feed feed : this.feeds) {
feedIds.add(feed.feedId);
}
}
adapter.setFeedIds(feedIds);
}
private void setAdapterData() {
adapter.setData(this.folderNames, this.folderChildren, this.feeds);
binding.listView.setVisibility(this.feeds.isEmpty() ? View.GONE : View.VISIBLE);
binding.textNoSubscriptions.setVisibility(this.feeds.isEmpty() ? View.VISIBLE : View.GONE);
}
}

View file

@ -1,296 +1,72 @@
package com.newsblur.activity;
import android.content.Context;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.CheckBox;
import android.widget.ExpandableListView;
import android.widget.ImageView;
import android.widget.TextView;
import com.newsblur.R;
import com.newsblur.domain.Feed;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedOrderFilter;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.FolderViewFilter;
import com.newsblur.util.ListOrderFilter;
import com.newsblur.util.PrefsUtils;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;
public class WidgetConfigAdapter extends BaseExpandableListAdapter {
private final static int defaultTextSizeChild = 14;
private final static int defaultTextSizeGroup = 13;
private Set<String> feedIds = new HashSet<>();
private ArrayList<String> folderNames = new ArrayList<>();
private ArrayList<ArrayList<Feed>> folderChildren = new ArrayList<>();
private FolderViewFilter folderViewFilter;
private ListOrderFilter listOrderFilter;
private FeedOrderFilter feedOrderFilter;
private float textSize;
public class WidgetConfigAdapter extends FeedChooserAdapter {
WidgetConfigAdapter(Context context) {
folderViewFilter = PrefsUtils.getWidgetConfigFolderView(context);
listOrderFilter = PrefsUtils.getWidgetConfigListOrder(context);
feedOrderFilter = PrefsUtils.getWidgetConfigFeedOrder(context);
textSize = PrefsUtils.getListTextSize(context);
}
@Override
public int getGroupCount() {
return folderNames.size();
}
@Override
public int getChildrenCount(int groupPosition) {
return folderChildren.get(groupPosition).size();
}
@Override
public String getGroup(int groupPosition) {
return folderNames.get(groupPosition);
}
@Override
public Feed getChild(int groupPosition, int childPosition) {
return folderChildren.get(groupPosition).get(childPosition);
}
@Override
public long getGroupId(int groupPosition) {
return folderNames.get(groupPosition).hashCode();
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return folderChildren.get(groupPosition).get(childPosition).hashCode();
}
@Override
public boolean hasStableIds() {
return true;
super(context);
}
@Override
public View getGroupView(final int groupPosition, boolean isExpanded, View convertView, final ViewGroup parent) {
String folderName = folderNames.get(groupPosition);
if (folderName.equals(AppConstants.ROOT_FOLDER)) {
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_root_folder, parent, false);
} else {
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_folder, parent, false);
TextView textName = convertView.findViewById(R.id.text_folder_name);
textName.setTextSize(textSize * defaultTextSizeGroup);
textName.setText(folderName);
}
View groupView = super.getGroupView(groupPosition, isExpanded, convertView, parent);
((ExpandableListView) parent).expandGroup(groupPosition);
convertView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ArrayList<Feed> folderChild = WidgetConfigAdapter.this.folderChildren.get(groupPosition);
// check all is selected
boolean allSelected = true;
for (Feed feed : folderChild) {
if (!feedIds.contains(feed.feedId)) {
allSelected = false;
break;
}
groupView.setOnClickListener(v -> {
ArrayList<Feed> folderChild = WidgetConfigAdapter.this.folderChildren.get(groupPosition);
// check all is selected
boolean allSelected = true;
for (Feed feed : folderChild) {
if (!feedIds.contains(feed.feedId)) {
allSelected = false;
break;
}
for (Feed feed : folderChild) {
if (allSelected) {
feedIds.remove(feed.feedId);
} else {
feedIds.add(feed.feedId);
}
}
setWidgetFeedIds(parent.getContext());
notifyDataChanged();
}
for (Feed feed : folderChild) {
if (allSelected) {
feedIds.remove(feed.feedId);
} else {
feedIds.add(feed.feedId);
}
}
setWidgetFeedIds(parent.getContext());
notifyDataChanged();
});
return convertView;
return groupView;
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, final ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_feed, parent, false);
}
View childView = super.getChildView(groupPosition, childPosition, isLastChild, convertView, parent);
final Feed feed = folderChildren.get(groupPosition).get(childPosition);
TextView textTitle = convertView.findViewById(R.id.text_title);
TextView textDetails = convertView.findViewById(R.id.text_details);
final CheckBox checkBox = convertView.findViewById(R.id.check_box);
ImageView img = convertView.findViewById(R.id.img);
textTitle.setTextSize(textSize * defaultTextSizeChild);
textDetails.setTextSize(textSize * defaultTextSizeChild);
textTitle.setText(feed.title);
checkBox.setChecked(feedIds.contains(feed.feedId));
final CheckBox checkBox = childView.findViewById(R.id.check_box);
final ImageView imgToggle = childView.findViewById(R.id.img_toggle);
checkBox.setVisibility(View.VISIBLE);
imgToggle.setVisibility(View.GONE);
if (feedOrderFilter == FeedOrderFilter.NAME || feedOrderFilter == FeedOrderFilter.OPENS) {
textDetails.setText(parent.getContext().getString(R.string.feed_opens, feed.feedOpens));
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS) {
textDetails.setText(parent.getContext().getString(R.string.feed_subscribers, feed.subscribers));
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH) {
textDetails.setText(parent.getContext().getString(R.string.feed_stories_per_month, feed.storiesPerMonth));
} else {
// FeedOrderFilter.RECENT_STORY
try {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
Date dateTime = dateFormat.parse(feed.lastStoryDate);
CharSequence relativeTimeString = DateUtils.getRelativeTimeSpanString(dateTime.getTime(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
textDetails.setText(relativeTimeString);
} catch (Exception e) {
textDetails.setText(feed.lastStoryDate);
}
}
FeedUtils.iconLoader.displayImage(feed.faviconUrl, img, 0, false, img.getHeight(), true);
convertView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
checkBox.setChecked(!checkBox.isChecked());
if (checkBox.isChecked()) {
feedIds.add(feed.feedId);
} else {
feedIds.remove(feed.feedId);
}
setWidgetFeedIds(parent.getContext());
childView.setOnClickListener(v -> {
checkBox.setChecked(!checkBox.isChecked());
if (checkBox.isChecked()) {
feedIds.add(feed.feedId);
} else {
feedIds.remove(feed.feedId);
}
setWidgetFeedIds(parent.getContext());
});
return convertView;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
@Override
public boolean areAllItemsEnabled() {
return super.areAllItemsEnabled();
}
void setData(ArrayList<String> activeFoldersNames, ArrayList<ArrayList<Feed>> activeFolderChildren, ArrayList<Feed> feeds) {
if (folderViewFilter == FolderViewFilter.NESTED) {
this.folderNames = activeFoldersNames;
this.folderChildren = activeFolderChildren;
} else {
this.folderNames = new ArrayList<>(1);
this.folderNames.add(AppConstants.ROOT_FOLDER);
this.folderChildren = new ArrayList<>();
this.folderChildren.add(feeds);
}
this.notifyDataChanged();
}
void replaceFeedOrder(FeedOrderFilter feedOrderFilter) {
this.feedOrderFilter = feedOrderFilter;
notifyDataChanged();
}
void replaceListOrder(ListOrderFilter listOrderFilter) {
this.listOrderFilter = listOrderFilter;
notifyDataChanged();
}
void replaceFolderView(FolderViewFilter folderViewFilter) {
this.folderViewFilter = folderViewFilter;
}
private void notifyDataChanged() {
for (ArrayList<Feed> feedList : this.folderChildren) {
Collections.sort(feedList, getListComparator());
}
this.notifyDataSetChanged();
}
void setFeedIds(Set<String> feedIds) {
this.feedIds.clear();
this.feedIds.addAll(feedIds);
}
void replaceFeedIds(Set<String> feedIds) {
setFeedIds(feedIds);
this.notifyDataSetChanged();
return childView;
}
private void setWidgetFeedIds(Context context) {
PrefsUtils.setWidgetFeedIds(context, feedIds);
}
private Comparator<Feed> getListComparator() {
return new Comparator<Feed>() {
@Override
public int compare(Feed o1, Feed o2) {
if (feedOrderFilter == FeedOrderFilter.NAME && listOrderFilter == ListOrderFilter.ASCENDING) {
return o1.title.compareTo(o2.title);
} else if (feedOrderFilter == FeedOrderFilter.NAME && listOrderFilter == ListOrderFilter.DESCENDING) {
return o2.title.compareTo(o1.title);
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS && listOrderFilter == ListOrderFilter.ASCENDING) {
return Integer.valueOf(o1.subscribers).compareTo(Integer.valueOf(o2.subscribers));
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS && listOrderFilter == ListOrderFilter.DESCENDING) {
return Integer.valueOf(o2.subscribers).compareTo(Integer.valueOf(o1.subscribers));
} else if (feedOrderFilter == FeedOrderFilter.OPENS && listOrderFilter == ListOrderFilter.ASCENDING) {
return Integer.compare(o1.feedOpens, o2.feedOpens);
} else if (feedOrderFilter == FeedOrderFilter.OPENS && listOrderFilter == ListOrderFilter.DESCENDING) {
return Integer.compare(o2.feedOpens, o1.feedOpens);
} else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY && listOrderFilter == ListOrderFilter.ASCENDING) {
return compareLastStoryDateTimes(o1.lastStoryDate, o2.lastStoryDate, listOrderFilter);
} else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY && listOrderFilter == ListOrderFilter.DESCENDING) {
return compareLastStoryDateTimes(o1.lastStoryDate, o2.lastStoryDate, listOrderFilter);
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH && listOrderFilter == ListOrderFilter.ASCENDING) {
return Integer.compare(o1.storiesPerMonth, o2.storiesPerMonth);
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH && listOrderFilter == ListOrderFilter.DESCENDING) {
return Integer.compare(o2.storiesPerMonth, o1.storiesPerMonth);
}
return o1.title.compareTo(o2.title);
}
};
}
private int compareLastStoryDateTimes(String firstDateTime, String secondDateTime, ListOrderFilter listOrderFilter) {
try {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
// found null last story date times on feeds
if (TextUtils.isEmpty(firstDateTime)) {
firstDateTime = "2000-01-01 00:00:00";
}
if (TextUtils.isEmpty(secondDateTime)) {
secondDateTime = "2000-01-01 00:00:00";
}
Date firstDate = dateFormat.parse(firstDateTime);
Date secondDate = dateFormat.parse(secondDateTime);
if (listOrderFilter == ListOrderFilter.ASCENDING) {
return firstDate.compareTo(secondDate);
} else {
return secondDate.compareTo(firstDate);
}
} catch (ParseException e) {
e.printStackTrace();
return 0;
}
}
}

View file

@ -6,9 +6,9 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.os.CancellationSignal;
import android.support.annotation.Nullable;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import androidx.annotation.Nullable;
import androidx.loader.content.AsyncTaskLoader;
import androidx.loader.content.Loader;
import android.text.TextUtils;
import android.util.Log;
@ -284,6 +284,19 @@ public class BlurDatabaseHelper {
return hashes;
}
public Set<String> getStarredStoryHashes() {
String q = "SELECT " + DatabaseConstants.STORY_HASH +
" FROM " + DatabaseConstants.STORY_TABLE +
" WHERE " + DatabaseConstants.STORY_STARRED + " = 1" ;
Cursor c = dbRO.rawQuery(q, null);
Set<String> hashes = new HashSet<>(c.getCount());
while (c.moveToNext()) {
hashes.add(c.getString(c.getColumnIndexOrThrow(DatabaseConstants.STORY_HASH)));
}
c.close();
return hashes;
}
public Set<String> getAllStoryImages() {
Cursor c = dbRO.query(DatabaseConstants.STORY_TABLE, new String[]{DatabaseConstants.STORY_IMAGE_URLS}, null, null, null, null, null);
Set<String> urls = new HashSet<String>(c.getCount());
@ -584,6 +597,22 @@ public class BlurDatabaseHelper {
}
}
public void markStoryHashesStarred(Collection<String> hashes, boolean isStarred) {
synchronized (RW_MUTEX) {
dbRW.beginTransaction();
try {
ContentValues values = new ContentValues();
values.put(DatabaseConstants.STORY_STARRED, isStarred);
for (String hash : hashes) {
dbRW.update(DatabaseConstants.STORY_TABLE, values, DatabaseConstants.STORY_HASH + " = ?", new String[]{hash});
}
dbRW.setTransactionSuccessful();
} finally {
dbRW.endTransaction();
}
}
}
public void setFeedsActive(Set<String> feedIds, boolean active) {
synchronized (RW_MUTEX) {
dbRW.beginTransaction();

View file

@ -75,6 +75,8 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
public int totalSocialNeutCount = 0;
/** Total positive unreads for all social feeds. */
public int totalSocialPosiCount = 0;
/** Total active feeds. */
public int totalActiveFeedCount = 0;
/** Feeds, indexed by feed ID. */
private Map<String,Feed> feeds = Collections.emptyMap();
@ -557,6 +559,7 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
feedPosCounts = new HashMap<String,Integer>();
totalNeutCount = 0;
totalPosCount = 0;
totalActiveFeedCount = 0;
while (cursor.moveToNext()) {
Feed f = Feed.fromCursor(cursor);
feeds.put(f.feedId, f);
@ -570,6 +573,9 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
feedNeutCounts.put(f.feedId, neut);
totalNeutCount += neut;
}
if (f.active) {
totalActiveFeedCount++;
}
}
recountFeeds();
notifyDataSetChanged();

View file

@ -4,7 +4,7 @@ import android.content.Context;
import android.database.Cursor;
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
import android.support.v4.content.AsyncTaskLoader;
import androidx.loader.content.AsyncTaskLoader;
import com.newsblur.util.AppConstants;

View file

@ -3,10 +3,10 @@ package com.newsblur.database;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.PagerAdapter;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.viewpager.widget.PagerAdapter;
import android.view.View;
import android.view.ViewGroup;
@ -178,7 +178,6 @@ public class ReadingAdapter extends PagerAdapter {
}
}
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
if (curTransaction == null) {
curTransaction = fm.beginTransaction();
}
@ -208,11 +207,9 @@ public class ReadingAdapter extends PagerAdapter {
if (fragment != lastActiveFragment) {
if (lastActiveFragment != null) {
lastActiveFragment.setMenuVisibility(false);
lastActiveFragment.setUserVisibleHint(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
}
lastActiveFragment = fragment;
}

View file

@ -4,8 +4,8 @@ import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Parcelable;
import android.support.v7.widget.RecyclerView;
import android.support.v7.util.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.DiffUtil;
import android.text.TextUtils;
import android.view.ContextMenu;
import android.view.GestureDetector;
@ -126,7 +126,11 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
}
public int getStoryCount() {
return stories.size();
if (fs != null && UIUtils.needsPremiumAccess(context, fs)) {
return Math.min(3, stories.size());
} else {
return stories.size();
}
}
/**
@ -464,6 +468,9 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
case GEST_ACTION_UNSAVE:
FeedUtils.setStorySaved(story, false, context, null);
break;
case GEST_ACTION_STATISTICS:
FeedUtils.openStatistics(context, story.feedId);
break;
case GEST_ACTION_NONE:
default:
}

View file

@ -2,7 +2,7 @@ package com.newsblur.domain;
import android.content.ContentValues;
import android.database.Cursor;
import android.support.annotation.Nullable;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import java.util.Collection;

View file

@ -6,12 +6,12 @@ import android.app.Dialog;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

View file

@ -5,7 +5,7 @@ import java.util.HashSet;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;

View file

@ -4,7 +4,7 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import androidx.fragment.app.DialogFragment;
import com.newsblur.R;

View file

@ -10,7 +10,7 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import androidx.fragment.app.DialogFragment;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;

View file

@ -15,7 +15,7 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import androidx.fragment.app.DialogFragment;
public class DeleteFeedFragment extends DialogFragment {
private static final String FEED_TYPE = "feed_type";

View file

@ -4,9 +4,9 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import android.text.TextUtils;
import com.newsblur.R;

Some files were not shown because too many files have changed in this diff Show more