Merge branch 'master' into grid
* master: (238 commits) Upping ios to v4.6.4. Including hidden stories in fetch. Also fixing send to button in story settings cog. Less ifs when handling volume key Add preference for volume key navigation Override volume key for next/prev navigation in reading activity Adding an include_hidden parameter to single feeds and rivers, defaulting to false. Clients that don't use hidden stories but use paging as an indicator will need to account for a hidden_stories_count field, as indicating in the API docs. Bumping Android version to 4.2.0b1. Disable debug. Updating oauth toolkit to fix admin issues. Better algorithm for image cache cleanup. (#627) Unescape HTML entities in image alt text. (#652) Redraw overlay buttons slightly less often when scrolling. Resource-ify sync status messages. Fix story sync status indication. (#644) Adding autocomplete ignores. Remove last traces of the legacy DB! Migrate comment replies off of legacy DB. Migrate user profiles off of legacy DB. Start migrating comments from legacy DB. Fix crash when updating old comments. ...
|
@ -1,7 +1,7 @@
|
|||
The MIT License
|
||||
===============
|
||||
|
||||
Copyright (c) 2009-2012 Samuel Clay, NewsBlur <samuel@newsblur.com>.
|
||||
Copyright (c) 2009 Samuel Clay, NewsBlur <samuel@newsblur.com>.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
@ -51,7 +51,7 @@ def opml_upload(request):
|
|||
folders = None
|
||||
ProcessOPML.delay(request.user.pk)
|
||||
feed_count = opml_importer.count_feeds_in_opml()
|
||||
logging.user(request, "~FR~SBOPML pload took too long, found %s feeds. Tasking..." % feed_count)
|
||||
logging.user(request, "~FR~SBOPML upload took too long, found %s feeds. Tasking..." % feed_count)
|
||||
payload = dict(folders=folders, delayed=True, feed_count=feed_count)
|
||||
code = 2
|
||||
message = ""
|
||||
|
|
|
@ -219,7 +219,7 @@ class Profile(models.Model):
|
|||
self.user.save()
|
||||
self.send_new_user_queue_email()
|
||||
|
||||
def setup_premium_history(self, alt_email=None):
|
||||
def setup_premium_history(self, alt_email=None, check_premium=False):
|
||||
paypal_payments = []
|
||||
stripe_payments = []
|
||||
existing_history = PaymentHistory.objects.filter(user=self.user,
|
||||
|
@ -282,6 +282,10 @@ class Profile(models.Model):
|
|||
logging.user(self.user, "~BY~SN~FWFound ~SB%s paypal~SN and ~SB%s stripe~SN payments (~SB%s payments expire: ~SN~FB%s~FW)" % (
|
||||
len(paypal_payments), len(stripe_payments), len(payment_history), self.premium_expire))
|
||||
|
||||
if (check_premium and not self.is_premium and
|
||||
(not self.premium_expire or self.premium_expire > datetime.datetime.now())):
|
||||
self.activate_premium()
|
||||
|
||||
def refund_premium(self, partial=False):
|
||||
refunded = False
|
||||
|
||||
|
@ -412,18 +416,26 @@ class Profile(models.Model):
|
|||
|
||||
logging.user(self.user, "~BB~FM~SBSending email for new user: %s" % self.user.email)
|
||||
|
||||
def send_opml_export_email(self):
|
||||
def send_opml_export_email(self, reason=None, force=False):
|
||||
if not self.user.email:
|
||||
return
|
||||
|
||||
MSentEmail.objects.get_or_create(receiver_user_id=self.user.pk,
|
||||
email_type='opml_export')
|
||||
emails_sent = MSentEmail.objects.filter(receiver_user_id=self.user.pk,
|
||||
email_type='opml_export')
|
||||
day_ago = datetime.datetime.now() - datetime.timedelta(days=1)
|
||||
for email in emails_sent:
|
||||
if email.date_sent > day_ago and not force:
|
||||
logging.user(self.user, "~SN~FMNot sending opml export email, already sent today.")
|
||||
return
|
||||
|
||||
MSentEmail.record(receiver_user_id=self.user.pk, email_type='opml_export')
|
||||
|
||||
exporter = OPMLExporter(self.user)
|
||||
opml = exporter.process()
|
||||
|
||||
params = {
|
||||
'feed_count': UserSubscription.objects.filter(user=self.user).count(),
|
||||
'reason': reason,
|
||||
}
|
||||
user = self.user
|
||||
text = render_to_string('mail/email_opml_export.txt', params)
|
||||
|
@ -689,7 +701,21 @@ NewsBlur""" % {'user': self.user.username, 'feeds': subs.count()}
|
|||
'secret': self.secret_token
|
||||
}) + ('?' + next + '=1' if next else '')
|
||||
|
||||
|
||||
|
||||
@classmethod
|
||||
def doublecheck_paypal_payments(cls, days=14):
|
||||
payments = PayPalIPN.objects.filter(txn_type='subscr_payment',
|
||||
updated_at__gte=datetime.datetime.now()
|
||||
- datetime.timedelta(days)
|
||||
).order_by('-created_at')
|
||||
for payment in payments:
|
||||
try:
|
||||
profile = Profile.objects.get(user__username=payment.custom)
|
||||
except Profile.DoesNotExist:
|
||||
logging.debug(" ---> ~FRCouldn't find user: ~SB~FC%s" % payment.custom)
|
||||
continue
|
||||
profile.setup_premium_history(check_premium=True)
|
||||
|
||||
|
||||
def create_profile(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
|
@ -723,11 +749,10 @@ def paypal_payment_history_sync(sender, **kwargs):
|
|||
user = User.objects.get(email__iexact=ipn_obj.payer_email)
|
||||
logging.user(user, "~BC~SB~FBPaypal subscription payment")
|
||||
try:
|
||||
user.profile.setup_premium_history()
|
||||
user.profile.setup_premium_history(check_premium=True)
|
||||
except:
|
||||
return {"code": -1, "message": "User doesn't exist."}
|
||||
payment_was_successful.connect(paypal_payment_history_sync)
|
||||
logging.debug(" ---> ~SN~FBHooking up signal ~SB%s~SN." % payment_was_successful.receivers)
|
||||
|
||||
def paypal_payment_was_flagged(sender, **kwargs):
|
||||
ipn_obj = sender
|
||||
|
@ -737,7 +762,7 @@ def paypal_payment_was_flagged(sender, **kwargs):
|
|||
user = User.objects.get(email__iexact=ipn_obj.payer_email)
|
||||
logging.user(user, "~BC~SB~FBPaypal subscription payment flagged")
|
||||
try:
|
||||
user.profile.setup_premium_history()
|
||||
user.profile.setup_premium_history(check_premium=True)
|
||||
except:
|
||||
return {"code": -1, "message": "User doesn't exist."}
|
||||
payment_was_flagged.connect(paypal_payment_was_flagged)
|
||||
|
@ -750,7 +775,7 @@ def paypal_recurring_payment_history_sync(sender, **kwargs):
|
|||
user = User.objects.get(email__iexact=ipn_obj.payer_email)
|
||||
logging.user(user, "~BC~SB~FBPaypal subscription recurring payment")
|
||||
try:
|
||||
user.profile.setup_premium_history()
|
||||
user.profile.setup_premium_history(check_premium=True)
|
||||
except:
|
||||
return {"code": -1, "message": "User doesn't exist."}
|
||||
recurring_payment.connect(paypal_recurring_payment_history_sync)
|
||||
|
@ -770,7 +795,7 @@ def stripe_payment_history_sync(sender, full_json, **kwargs):
|
|||
try:
|
||||
profile = Profile.objects.get(stripe_id=stripe_id)
|
||||
logging.user(profile.user, "~BC~SB~FBStripe subscription payment")
|
||||
profile.setup_premium_history()
|
||||
profile.setup_premium_history(check_premium=True)
|
||||
except Profile.DoesNotExist:
|
||||
return {"code": -1, "message": "User doesn't exist."}
|
||||
zebra_webhook_charge_succeeded.connect(stripe_payment_history_sync)
|
||||
|
|
|
@ -534,7 +534,7 @@ def delete_starred_stories(request):
|
|||
@ajax_login_required
|
||||
@json.json_view
|
||||
def delete_all_sites(request):
|
||||
request.user.profile.send_opml_export_email()
|
||||
request.user.profile.send_opml_export_email(reason="You have deleted all of your sites, so here's a backup just in case.")
|
||||
|
||||
subs = UserSubscription.objects.filter(user=request.user)
|
||||
sub_count = subs.count()
|
||||
|
|
|
@ -752,6 +752,18 @@ class UserSubscription(models.Model):
|
|||
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def score_story(scores):
|
||||
max_score = max(scores['author'], scores['tags'], scores['title'])
|
||||
min_score = min(scores['author'], scores['tags'], scores['title'])
|
||||
|
||||
if max_score > 0:
|
||||
return 1
|
||||
elif min_score < 0:
|
||||
return -1
|
||||
|
||||
return scores['feed']
|
||||
|
||||
def switch_feed(self, new_feed, old_feed):
|
||||
# Rewrite feed in subscription folders
|
||||
try:
|
||||
|
@ -1298,7 +1310,15 @@ class UserSubscriptionFolders(models.Model):
|
|||
UserSubscription.objects.filter(user=self.user, feed__in=feeds_to_delete).delete()
|
||||
|
||||
return deleted_folder
|
||||
|
||||
def delete_feeds_by_folder(self, feeds_by_folder):
|
||||
logging.user(self.user, "~FBDeleting ~FR~SB%s~SN feeds~FB: ~SB%s" % (
|
||||
len(feeds_by_folder), feeds_by_folder))
|
||||
for feed_id, in_folder in feeds_by_folder:
|
||||
self.delete_feed(feed_id, in_folder)
|
||||
|
||||
return self
|
||||
|
||||
def rename_folder(self, folder_to_rename, new_folder_name, in_folder):
|
||||
def _find_folder_in_folders(old_folders, folder_name):
|
||||
new_folders = []
|
||||
|
@ -1321,6 +1341,20 @@ class UserSubscriptionFolders(models.Model):
|
|||
self.folders = json.encode(user_sub_folders)
|
||||
self.save()
|
||||
|
||||
def move_feed_to_folders(self, feed_id, in_folders=None, to_folders=None):
|
||||
logging.user(self.user, "~FBMoving feed '~SB%s~SN' in '%s' to: ~SB%s" % (
|
||||
feed_id, in_folders, to_folders))
|
||||
user_sub_folders = json.decode(self.folders)
|
||||
for in_folder in in_folders:
|
||||
self.delete_feed(feed_id, in_folder, commit_delete=False)
|
||||
user_sub_folders = json.decode(self.folders)
|
||||
for to_folder in to_folders:
|
||||
user_sub_folders = add_object_to_folder(int(feed_id), to_folder, user_sub_folders)
|
||||
self.folders = json.encode(user_sub_folders)
|
||||
self.save()
|
||||
|
||||
return self
|
||||
|
||||
def move_feed_to_folder(self, feed_id, in_folder=None, to_folder=None):
|
||||
logging.user(self.user, "~FBMoving feed '~SB%s~SN' in '%s' to: ~SB%s" % (
|
||||
feed_id, in_folder, to_folder))
|
||||
|
@ -1345,6 +1379,14 @@ class UserSubscriptionFolders(models.Model):
|
|||
|
||||
return self
|
||||
|
||||
def move_feeds_by_folder_to_folder(self, feeds_by_folder, to_folder):
|
||||
logging.user(self.user, "~FBMoving ~SB%s~SN feeds to folder: ~SB%s" % (
|
||||
len(feeds_by_folder), to_folder))
|
||||
for feed_id, in_folder in feeds_by_folder:
|
||||
self.move_feed_to_folder(feed_id, in_folder, to_folder)
|
||||
|
||||
return self
|
||||
|
||||
def rewrite_feed(self, original_feed, duplicate_feed):
|
||||
def rewrite_folders(folders, original_feed, duplicate_feed):
|
||||
new_folders = []
|
||||
|
|
|
@ -35,12 +35,15 @@ urlpatterns = patterns('',
|
|||
url(r'^mark_story_hash_as_unstarred', views.mark_story_hash_as_unstarred),
|
||||
url(r'^mark_feed_as_read', views.mark_feed_as_read),
|
||||
url(r'^delete_feed_by_url', views.delete_feed_by_url, name='delete-feed-by-url'),
|
||||
url(r'^delete_feeds_by_folder', views.delete_feeds_by_folder, name='delete-feeds-by-folder'),
|
||||
url(r'^delete_feed', views.delete_feed, name='delete-feed'),
|
||||
url(r'^delete_folder', views.delete_folder, name='delete-folder'),
|
||||
url(r'^rename_feed', views.rename_feed, name='rename-feed'),
|
||||
url(r'^rename_folder', views.rename_folder, name='rename-folder'),
|
||||
url(r'^move_feed_to_folders', views.move_feed_to_folders, name='move-feed-to-folders'),
|
||||
url(r'^move_feed_to_folder', views.move_feed_to_folder, name='move-feed-to-folder'),
|
||||
url(r'^move_folder_to_folder', views.move_folder_to_folder, name='move-folder-to-folder'),
|
||||
url(r'^move_feeds_by_folder_to_folder', views.move_feeds_by_folder_to_folder, name='move-feeds-by-folder-to-folder'),
|
||||
url(r'^add_url', views.add_url),
|
||||
url(r'^add_folder', views.add_folder),
|
||||
url(r'^add_feature', views.add_feature, name='add-feature'),
|
||||
|
|
|
@ -522,6 +522,7 @@ def load_single_feed(request, feed_id):
|
|||
read_filter = request.REQUEST.get('read_filter', 'all')
|
||||
query = request.REQUEST.get('query')
|
||||
include_story_content = is_true(request.REQUEST.get('include_story_content', True))
|
||||
include_hidden = is_true(request.REQUEST.get('include_hidden', False))
|
||||
message = None
|
||||
user_search = None
|
||||
|
||||
|
@ -596,7 +597,7 @@ def load_single_feed(request, feed_id):
|
|||
usersubs=[usersub],
|
||||
group_by_feed=False,
|
||||
cutoff_date=user.profile.unread_cutoff)
|
||||
story_hashes = [story['story_hash'] for story in stories]
|
||||
story_hashes = [story['story_hash'] for story in stories if story['story_hash']]
|
||||
starred_stories = MStarredStory.objects(user_id=user.pk,
|
||||
story_feed_id=feed.pk,
|
||||
story_hash__in=story_hashes)\
|
||||
|
@ -650,7 +651,8 @@ def load_single_feed(request, feed_id):
|
|||
'tags': apply_classifier_tags(classifier_tags, story),
|
||||
'title': apply_classifier_titles(classifier_titles, story),
|
||||
}
|
||||
|
||||
story['score'] = UserSubscription.score_story(story['intelligence'])
|
||||
|
||||
# Intelligence
|
||||
feed_tags = json.decode(feed.data.popular_tags) if feed.data.popular_tags else []
|
||||
feed_authors = json.decode(feed.data.popular_authors) if feed.data.popular_authors else []
|
||||
|
@ -674,6 +676,16 @@ def load_single_feed(request, feed_id):
|
|||
search_log = "~SN~FG(~SB%s~SN) " % query if query else ""
|
||||
logging.user(request, "~FYLoading feed: ~SB%s%s (%s/%s) %s%s" % (
|
||||
feed.feed_title[:22], ('~SN/p%s' % page) if page > 1 else '', order, read_filter, search_log, time_breakdown))
|
||||
|
||||
if not include_hidden:
|
||||
hidden_stories_removed = 0
|
||||
new_stories = []
|
||||
for story in stories:
|
||||
if story['score'] >= 0:
|
||||
new_stories.append(story)
|
||||
else:
|
||||
hidden_stories_removed += 1
|
||||
stories = new_stories
|
||||
|
||||
data = dict(stories=stories,
|
||||
user_profiles=user_profiles,
|
||||
|
@ -686,6 +698,7 @@ def load_single_feed(request, feed_id):
|
|||
elapsed_time=round(float(timediff), 2),
|
||||
message=message)
|
||||
|
||||
if not include_hidden: data['hidden_stories_removed'] = hidden_stories_removed
|
||||
if dupe_feed_id: data['dupe_feed_id'] = dupe_feed_id
|
||||
if not usersub:
|
||||
data.update(feed.canonical())
|
||||
|
@ -1023,6 +1036,7 @@ def load_river_stories__redis(request):
|
|||
order = request.REQUEST.get('order', 'newest')
|
||||
read_filter = request.REQUEST.get('read_filter', 'unread')
|
||||
query = request.REQUEST.get('query')
|
||||
include_hidden = is_true(request.REQUEST.get('include_hidden', False))
|
||||
now = localtime_for_timezone(datetime.datetime.now(), user.profile.timezone)
|
||||
usersubs = []
|
||||
code = 1
|
||||
|
@ -1157,6 +1171,8 @@ def load_river_stories__redis(request):
|
|||
'tags': apply_classifier_tags(classifier_tags, story),
|
||||
'title': apply_classifier_titles(classifier_titles, story),
|
||||
}
|
||||
story['score'] = UserSubscription.score_story(story['intelligence'])
|
||||
|
||||
|
||||
if not user.profile.is_premium:
|
||||
message = "The full River of News is a premium feature."
|
||||
|
@ -1171,17 +1187,33 @@ def load_river_stories__redis(request):
|
|||
"stories, ~SN%s/%s/%s feeds, %s/%s)" %
|
||||
(page, len(stories), len(mstories), len(found_feed_ids),
|
||||
len(feed_ids), len(original_feed_ids), order, read_filter))
|
||||
|
||||
|
||||
if not include_hidden:
|
||||
hidden_stories_removed = 0
|
||||
new_stories = []
|
||||
for story in stories:
|
||||
if story['score'] >= 0:
|
||||
new_stories.append(story)
|
||||
else:
|
||||
hidden_stories_removed += 1
|
||||
stories = new_stories
|
||||
|
||||
# if page <= 1:
|
||||
# import random
|
||||
# time.sleep(random.randint(0, 6))
|
||||
|
||||
return dict(code=code,
|
||||
data = dict(code=code,
|
||||
message=message,
|
||||
stories=stories,
|
||||
classifiers=classifiers,
|
||||
elapsed_time=timediff,
|
||||
user_search=user_search,
|
||||
user_profiles=user_profiles)
|
||||
|
||||
if not include_hidden: data['hidden_stories_removed'] = hidden_stories_removed
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@json.json_view
|
||||
|
@ -1519,7 +1551,12 @@ def mark_feed_as_read(request):
|
|||
for feed_id in feed_ids:
|
||||
if 'social:' in feed_id:
|
||||
user_id = int(feed_id.replace('social:', ''))
|
||||
sub = MSocialSubscription.objects.get(user_id=request.user.pk, subscription_user_id=user_id)
|
||||
try:
|
||||
sub = MSocialSubscription.objects.get(user_id=request.user.pk,
|
||||
subscription_user_id=user_id)
|
||||
except MSocialSubscription.DoesNotExist:
|
||||
logging.user(request, "~FRCouldn't find socialsub: %s" % user_id)
|
||||
continue
|
||||
if not multiple:
|
||||
sub_user = User.objects.get(pk=sub.subscription_user_id)
|
||||
logging.user(request, "~FMMarking social feed as read: ~SB%s" % (sub_user.username,))
|
||||
|
@ -1554,7 +1591,10 @@ def mark_feed_as_read(request):
|
|||
if multiple:
|
||||
logging.user(request, "~FMMarking ~SB%s~SN feeds as read" % len(feed_ids))
|
||||
r.publish(request.user.username, 'refresh:%s' % ','.join(feed_ids))
|
||||
|
||||
|
||||
if errors:
|
||||
logging.user(request, "~FMMarking read had errors: ~FR%s" % errors)
|
||||
|
||||
return dict(code=code, errors=errors, cutoff_date=cutoff_date, direction=direction)
|
||||
|
||||
def _parse_user_info(user):
|
||||
|
@ -1605,7 +1645,7 @@ def add_url(request):
|
|||
def add_folder(request):
|
||||
folder = request.POST['folder']
|
||||
parent_folder = request.POST.get('parent_folder', '')
|
||||
|
||||
folders = None
|
||||
logging.user(request, "~FRAdding Folder: ~SB%s (in %s)" % (folder, parent_folder))
|
||||
|
||||
if folder:
|
||||
|
@ -1613,13 +1653,14 @@ def add_folder(request):
|
|||
message = ""
|
||||
user_sub_folders_object, _ = UserSubscriptionFolders.objects.get_or_create(user=request.user)
|
||||
user_sub_folders_object.add_folder(parent_folder, folder)
|
||||
folders = json.decode(user_sub_folders_object.folders)
|
||||
r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL)
|
||||
r.publish(request.user.username, 'reload:feeds')
|
||||
else:
|
||||
code = -1
|
||||
message = "Gotta write in a folder name."
|
||||
|
||||
return dict(code=code, message=message)
|
||||
return dict(code=code, message=message, folders=folders)
|
||||
|
||||
@ajax_login_required
|
||||
@json.json_view
|
||||
|
@ -1672,18 +1713,39 @@ def delete_folder(request):
|
|||
in_folder = request.POST.get('in_folder', None)
|
||||
feed_ids_in_folder = [int(f) for f in request.REQUEST.getlist('feed_id') if f]
|
||||
|
||||
request.user.profile.send_opml_export_email()
|
||||
request.user.profile.send_opml_export_email(reason="You have deleted an entire folder of feeds, so here's a backup just in case.")
|
||||
|
||||
# Works piss poor with duplicate folder titles, if they are both in the same folder.
|
||||
# Deletes all, but only in the same folder parent. But nobody should be doing that, right?
|
||||
user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user)
|
||||
user_sub_folders.delete_folder(folder_to_delete, in_folder, feed_ids_in_folder)
|
||||
folders = json.decode(user_sub_folders.folders)
|
||||
|
||||
r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL)
|
||||
r.publish(request.user.username, 'reload:feeds')
|
||||
|
||||
return dict(code=1)
|
||||
return dict(code=1, folders=folders)
|
||||
|
||||
|
||||
@required_params('feeds_by_folder')
|
||||
@ajax_login_required
|
||||
@json.json_view
|
||||
def delete_feeds_by_folder(request):
|
||||
feeds_by_folder = json.decode(request.POST['feeds_by_folder'])
|
||||
|
||||
request.user.profile.send_opml_export_email(reason="You have deleted a number of feeds at once, so here's a backup just in case.")
|
||||
|
||||
# Works piss poor with duplicate folder titles, if they are both in the same folder.
|
||||
# Deletes all, but only in the same folder parent. But nobody should be doing that, right?
|
||||
user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user)
|
||||
user_sub_folders.delete_feeds_by_folder(feeds_by_folder)
|
||||
folders = json.decode(user_sub_folders.folders)
|
||||
|
||||
r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL)
|
||||
r.publish(request.user.username, 'reload:feeds')
|
||||
|
||||
return dict(code=1, folders=folders)
|
||||
|
||||
@ajax_login_required
|
||||
@json.json_view
|
||||
def rename_feed(request):
|
||||
|
@ -1718,6 +1780,22 @@ def rename_folder(request):
|
|||
|
||||
return dict(code=code)
|
||||
|
||||
@ajax_login_required
|
||||
@json.json_view
|
||||
def move_feed_to_folders(request):
|
||||
feed_id = int(request.POST['feed_id'])
|
||||
in_folders = request.POST.getlist('in_folders', '')
|
||||
to_folders = request.POST.getlist('to_folders', '')
|
||||
|
||||
user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user)
|
||||
user_sub_folders = user_sub_folders.move_feed_to_folders(feed_id, in_folders=in_folders,
|
||||
to_folders=to_folders)
|
||||
|
||||
r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL)
|
||||
r.publish(request.user.username, 'reload:feeds')
|
||||
|
||||
return dict(code=1, folders=json.decode(user_sub_folders.folders))
|
||||
|
||||
@ajax_login_required
|
||||
@json.json_view
|
||||
def move_feed_to_folder(request):
|
||||
|
@ -1726,7 +1804,8 @@ def move_feed_to_folder(request):
|
|||
to_folder = request.POST.get('to_folder', '')
|
||||
|
||||
user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user)
|
||||
user_sub_folders = user_sub_folders.move_feed_to_folder(feed_id, in_folder=in_folder, to_folder=to_folder)
|
||||
user_sub_folders = user_sub_folders.move_feed_to_folder(feed_id, in_folder=in_folder,
|
||||
to_folder=to_folder)
|
||||
|
||||
r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL)
|
||||
r.publish(request.user.username, 'reload:feeds')
|
||||
|
@ -1747,6 +1826,29 @@ def move_folder_to_folder(request):
|
|||
r.publish(request.user.username, 'reload:feeds')
|
||||
|
||||
return dict(code=1, folders=json.decode(user_sub_folders.folders))
|
||||
|
||||
@required_params('feeds_by_folder', 'to_folder')
|
||||
@ajax_login_required
|
||||
@json.json_view
|
||||
def move_feeds_by_folder_to_folder(request):
|
||||
feeds_by_folder = json.decode(request.POST['feeds_by_folder'])
|
||||
to_folder = request.POST['to_folder']
|
||||
new_folder = request.POST.get('new_folder', None)
|
||||
|
||||
request.user.profile.send_opml_export_email(reason="You have moved a number of feeds at once, so here's a backup just in case.")
|
||||
|
||||
user_sub_folders = get_object_or_404(UserSubscriptionFolders, user=request.user)
|
||||
|
||||
if new_folder:
|
||||
user_sub_folders.add_folder(to_folder, new_folder)
|
||||
to_folder = new_folder
|
||||
|
||||
user_sub_folders = user_sub_folders.move_feeds_by_folder_to_folder(feeds_by_folder, to_folder)
|
||||
|
||||
r = redis.Redis(connection_pool=settings.REDIS_PUBSUB_POOL)
|
||||
r.publish(request.user.username, 'reload:feeds')
|
||||
|
||||
return dict(code=1, folders=json.decode(user_sub_folders.folders))
|
||||
|
||||
@login_required
|
||||
def add_feature(request):
|
||||
|
@ -1969,7 +2071,7 @@ def _mark_story_as_starred(request):
|
|||
MStarredStoryCounts.count_for_user(request.user.pk, total_only=True)
|
||||
starred_counts, starred_count = MStarredStoryCounts.user_counts(request.user.pk, include_total=True)
|
||||
if not starred_count and len(starred_counts):
|
||||
starred_count = MStarredStory.objects(user_id=user.pk).count()
|
||||
starred_count = MStarredStory.objects(user_id=request.user.pk).count()
|
||||
|
||||
if created:
|
||||
logging.user(request, "~FCStarring: ~SB%s (~FM~SB%s~FC~SN)" % (story.story_title[:32], starred_story.user_tags))
|
||||
|
|
|
@ -152,6 +152,10 @@ class Feed(models.Model):
|
|||
'num_subscribers': self.num_subscribers,
|
||||
'updated': relative_timesince(self.last_update),
|
||||
'updated_seconds_ago': seconds_timesince(self.last_update),
|
||||
'last_story_date': self.last_story_date,
|
||||
'last_story_seconds_ago': seconds_timesince(self.last_story_date),
|
||||
'stories_last_month': self.stories_last_month,
|
||||
'average_stories_per_month': self.average_stories_per_month,
|
||||
'min_to_decay': self.min_to_decay,
|
||||
'subs': self.num_subscribers,
|
||||
'is_push': self.is_push,
|
||||
|
|
|
@ -23,6 +23,14 @@ from utils.view_functions import get_argument_or_404
|
|||
from utils.view_functions import required_params
|
||||
from vendor.timezones.utilities import localtime_for_timezone
|
||||
|
||||
|
||||
IGNORE_AUTOCOMPLETE = [
|
||||
"facebook.com/feeds/notifications.php",
|
||||
"inbox",
|
||||
"secret",
|
||||
"password",
|
||||
]
|
||||
|
||||
@json.json_view
|
||||
def search_feed(request):
|
||||
address = request.REQUEST.get('address')
|
||||
|
@ -106,7 +114,8 @@ def feed_autocomplete(request):
|
|||
|
||||
feeds = list(set([Feed.get_by_id(feed_id) for feed_id in feed_ids]))
|
||||
feeds = [feed for feed in feeds if feed and not feed.branch_from_feed]
|
||||
feeds = [feed for feed in feeds if 'facebook.com/feeds/notifications.php' not in feed.feed_address]
|
||||
feeds = [feed for feed in feeds if all([x not in feed.feed_address for x in IGNORE_AUTOCOMPLETE])]
|
||||
|
||||
if format == 'autocomplete':
|
||||
feeds = [{
|
||||
'id': feed.pk,
|
||||
|
|
|
@ -140,14 +140,18 @@ class MUserSearch(mongo.Document):
|
|||
feed.index_stories_for_search()
|
||||
|
||||
@classmethod
|
||||
def remove_all(cls):
|
||||
def remove_all(cls, drop_index=False):
|
||||
user_searches = cls.objects.all()
|
||||
logging.info(" ---> ~SN~FRRemoving ~SB%s~SN user searches..." % user_searches.count())
|
||||
for user_search in user_searches:
|
||||
user_search.remove()
|
||||
|
||||
logging.info(" ---> ~FRRemoving stories search index...")
|
||||
SearchStory.drop()
|
||||
try:
|
||||
user_search.remove()
|
||||
except Exception, e:
|
||||
print " ****> Error on search removal: %s" % e
|
||||
|
||||
if drop_index:
|
||||
logging.info(" ---> ~FRRemoving stories search index...")
|
||||
SearchStory.drop()
|
||||
|
||||
def remove(self):
|
||||
from apps.rss_feeds.models import Feed
|
||||
|
@ -388,6 +392,7 @@ class SearchFeed:
|
|||
|
||||
@classmethod
|
||||
def query(cls, text):
|
||||
cls.create_elasticsearch_mapping()
|
||||
try:
|
||||
cls.ES.default_indices = cls.index_name()
|
||||
cls.ES.indices.refresh()
|
||||
|
|
|
@ -2613,7 +2613,7 @@ class MSocialServices(mongo.Document):
|
|||
try:
|
||||
api = self.twitter_api()
|
||||
me = api.me()
|
||||
except tweepy.TweepError, e:
|
||||
except (tweepy.TweepError, TypeError), e:
|
||||
logging.user(user, "~FRException (%s): ~FCsetting to blank profile photo" % e)
|
||||
self.twitter_picture_url = None
|
||||
self.set_photo("nothing")
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry kind="src" path="gen"/>
|
||||
<classpathentry combineaccessrules="false" kind="src" path="/NewsBlur"/>
|
||||
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
|
||||
<classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
|
||||
<classpathentry kind="output" path="bin/classes"/>
|
||||
</classpath>
|
3
clients/android/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
.classpath
|
||||
.project
|
||||
NewsBlurTest/
|
|
@ -1,34 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>NewsBlurTest</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
<project>NewsBlur</project>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry kind="src" path="gen"/>
|
||||
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
|
||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
|
||||
<classpathentry kind="output" path="bin/classes"/>
|
||||
</classpath>
|
3
clients/android/NewsBlur/.gitignore
vendored
|
@ -12,3 +12,6 @@ bin/
|
|||
.metadata/
|
||||
gen/
|
||||
libs/ActionBarSherlock/
|
||||
.settings/
|
||||
.classpath
|
||||
.project
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>NewsBlur</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
<project>ActionBarSherlock</project>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
|
@ -1,11 +0,0 @@
|
|||
eclipse.preferences.version=1
|
||||
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
|
||||
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
|
||||
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
|
||||
org.eclipse.jdt.core.compiler.compliance=1.6
|
||||
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
|
||||
org.eclipse.jdt.core.compiler.debug.localVariable=generate
|
||||
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
|
||||
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
||||
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
||||
org.eclipse.jdt.core.compiler.source=1.6
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.newsblur"
|
||||
android:versionCode="80"
|
||||
android:versionName="4.0.2" >
|
||||
android:versionCode="85"
|
||||
android:versionName="4.2.0b1" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="11"
|
||||
|
@ -120,12 +120,6 @@
|
|||
|
||||
<receiver android:name=".service.ServiceScheduleReceiver" />
|
||||
|
||||
<provider
|
||||
android:name=".database.FeedProvider"
|
||||
android:authorities="com.newsblur"
|
||||
android:exported="true"
|
||||
android:multiprocess="true" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -2,11 +2,9 @@
|
|||
|
||||
*an abridged version of the official guide found [here](https://developer.android.com/tools/building/building-cmdline.html)*
|
||||
|
||||
1. install java and ant
|
||||
2. download the Android SDK from [android.com](https://developer.android.com/sdk/index.html) (the full ADT bundle in fine, too)
|
||||
3. get the `tools/` and/or `platform-tools/` directories on your path
|
||||
4. `android update sdk --no-ui` (this could take a while)
|
||||
5. go to both of the following NewsBlur directories and run `android update project --path .`:
|
||||
* `NewsBlur/media/android/NewsBlur/`
|
||||
* `NewsBlur/media/android/NewsBlur/libs/ActionBarSherlock/`
|
||||
6. build a test APK with `ant clean && ant debug` from `NewsBlur/media/android/NewsBlur/` (.apk will be in `NewsBlur/media/android/NewsBlur/bin/`)
|
||||
1. install java and ant (prefer official JDK over OpenJDK)
|
||||
2. download the Android SDK from [android.com](https://developer.android.com/sdk/index.html)
|
||||
3. get the `tools/` and/or `platform-tools/` directories ifrom the SDK on your path
|
||||
4. `android update sdk --no-ui` (this could take a while; you can use the --filter option to just get the SDK, platform tools, and support libs)
|
||||
5. go to the clients/android/ NewsBlur directory and run `android update project --name NewsBlur --path .`
|
||||
6. build a test APK with `ant clean && ant debug` (.apk will be in `/bin` under the working directory)
|
||||
|
|
|
@ -58,6 +58,13 @@
|
|||
android:key="immersive_enter_single_tap"
|
||||
android:title="@string/settings_immersive_enter_single_tap" >
|
||||
</CheckBoxPreference>
|
||||
<ListPreference
|
||||
android:key="volume_key_navigation"
|
||||
android:title="@string/volume_key_navigation"
|
||||
android:dialogTitle="@string/volume_key_navigation"
|
||||
android:entries="@array/volume_key_navigation_entries"
|
||||
android:entryValues="@array/volume_key_navigation_values"
|
||||
android:defaultValue="@string/default_volume_key_navigation_value" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
|
|
|
@ -250,4 +250,29 @@
|
|||
<item>dark</item>
|
||||
</string-array>
|
||||
<string name="default_theme_value">light</string>
|
||||
|
||||
<string name="sync_status_housekeeping">Tidying up...</string>
|
||||
<string name="sync_status_actions">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_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>
|
||||
<string name="sync_status_images">Storing %s images...</string>
|
||||
|
||||
<string name="volume_key_navigation">Volume Key Navigation</string>
|
||||
<string name="off">Off</string>
|
||||
<string name="volume_up_next">Up = Next Story</string>
|
||||
<string name="volume_down_next">Down = Next Story</string>
|
||||
<string-array name="volume_key_navigation_entries">
|
||||
<item>@string/off</item>
|
||||
<item>@string/volume_up_next</item>
|
||||
<item>@string/volume_down_next</item>
|
||||
</string-array>
|
||||
<string-array name="volume_key_navigation_values">
|
||||
<item>OFF</item>
|
||||
<item>UP_NEXT</item>
|
||||
<item>DOWN_NEXT</item>
|
||||
</string-array>
|
||||
<string name="default_volume_key_navigation_value">OFF</string>
|
||||
</resources>
|
||||
|
|
|
@ -48,7 +48,7 @@ public class AllSharedStoriesItemsList extends ItemsList {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected StoryOrder getStoryOrder() {
|
||||
public StoryOrder getStoryOrder() {
|
||||
return PrefsUtils.getStoryOrderForFolder(this, PrefConstants.ALL_SHARED_STORIES_FOLDER_NAME);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,9 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.content.CursorLoader;
|
||||
import android.content.Loader;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.database.MixedFeedsReadingAdapter;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefConstants;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
|
||||
public class AllSharedStoriesReading extends Reading {
|
||||
|
||||
|
@ -22,7 +14,7 @@ public class AllSharedStoriesReading extends Reading {
|
|||
setTitle(getResources().getString(R.string.all_shared_stories));
|
||||
|
||||
// No sourceUserId since this is all shared stories. The sourceUsedId for each story will be used.
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), getContentResolver(), defaultFeedView, null);
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), defaultFeedView, null);
|
||||
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ public class AllStoriesItemsList extends ItemsList implements MarkAllReadDialogL
|
|||
}
|
||||
|
||||
@Override
|
||||
protected StoryOrder getStoryOrder() {
|
||||
public StoryOrder getStoryOrder() {
|
||||
return PrefsUtils.getStoryOrderForFolder(this, PrefConstants.ALL_STORIES_FOLDER_NAME);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,9 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.content.CursorLoader;
|
||||
import android.content.Loader;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.database.MixedFeedsReadingAdapter;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefConstants;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
|
||||
public class AllStoriesReading extends Reading {
|
||||
|
||||
|
@ -20,7 +12,7 @@ public class AllStoriesReading extends Reading {
|
|||
super.onCreate(savedInstanceBundle);
|
||||
|
||||
setTitle(getResources().getString(R.string.all_stories_row_title));
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), getContentResolver(), defaultFeedView, null);
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), defaultFeedView, null);
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ public class FeedItemsList extends ItemsList {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected StoryOrder getStoryOrder() {
|
||||
public StoryOrder getStoryOrder() {
|
||||
return PrefsUtils.getStoryOrderForFeed(this, feed.feedId);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,12 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.content.CursorLoader;
|
||||
import android.content.Loader;
|
||||
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.database.FeedProvider;
|
||||
import com.newsblur.database.FeedReadingAdapter;
|
||||
import com.newsblur.domain.Classifier;
|
||||
import com.newsblur.domain.Feed;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
|
||||
public class FeedReading extends Reading {
|
||||
|
||||
|
@ -25,10 +17,7 @@ public class FeedReading extends Reading {
|
|||
feed = (Feed) getIntent().getSerializableExtra(EXTRA_FEED);
|
||||
super.onCreate(savedInstanceBundle);
|
||||
|
||||
Uri classifierUri = FeedProvider.CLASSIFIER_URI.buildUpon().appendPath(feed.feedId).build();
|
||||
Cursor feedClassifierCursor = contentResolver.query(classifierUri, null, null, null, null);
|
||||
Classifier classifier = Classifier.fromCursor(feedClassifierCursor);
|
||||
feedClassifierCursor.close();
|
||||
Classifier classifier = FeedUtils.dbHelper.getClassifierForFeed(feed.feedId);
|
||||
|
||||
setTitle(feed.title);
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ public class FolderItemsList extends ItemsList implements MarkAllReadDialogListe
|
|||
|
||||
@Override
|
||||
protected FeedSet createFeedSet() {
|
||||
return FeedSet.folder(this.folderName, dbHelper.getFeedsForFolder(folderName));
|
||||
return FeedSet.folder(this.folderName, FeedUtils.dbHelper.getFeedsForFolder(folderName));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -76,7 +76,7 @@ public class FolderItemsList extends ItemsList implements MarkAllReadDialogListe
|
|||
}
|
||||
|
||||
@Override
|
||||
protected StoryOrder getStoryOrder() {
|
||||
public StoryOrder getStoryOrder() {
|
||||
return PrefsUtils.getStoryOrderForFolder(this, folderName);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.content.CursorLoader;
|
||||
import android.content.Loader;
|
||||
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.database.MixedFeedsReadingAdapter;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
|
||||
public class FolderReading extends Reading {
|
||||
|
||||
|
@ -21,7 +15,7 @@ public class FolderReading extends Reading {
|
|||
folderName = getIntent().getStringExtra(Reading.EXTRA_FOLDERNAME);
|
||||
setTitle(folderName);
|
||||
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), getContentResolver(), defaultFeedView, null);
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), defaultFeedView, null);
|
||||
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
|
|
@ -44,8 +44,6 @@ public abstract class ItemsList extends NbActivity implements StateChangedListen
|
|||
|
||||
private FeedSet fs;
|
||||
|
||||
protected boolean stopLoading = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
@ -78,34 +76,11 @@ public abstract class ItemsList extends NbActivity implements StateChangedListen
|
|||
protected void onResume() {
|
||||
super.onResume();
|
||||
updateStatusIndicators();
|
||||
stopLoading = false;
|
||||
// this view shows stories, it is not safe to perform cleanup
|
||||
NBSyncService.holdStories(true);
|
||||
// Reading activities almost certainly changed the read/unread state of some stories. Ensure
|
||||
// we reflect those changes promptly.
|
||||
itemListFragment.hasUpdated();
|
||||
}
|
||||
|
||||
private void getFirstStories() {
|
||||
stopLoading = false;
|
||||
triggerRefresh(AppConstants.READING_STORY_PRELOAD, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
stopLoading = true;
|
||||
NBSyncService.holdStories(false);
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
public void triggerRefresh(int desiredStoryCount, int totalSeen) {
|
||||
if (!stopLoading) {
|
||||
boolean gotSome = NBSyncService.requestMoreForFeed(fs, desiredStoryCount, totalSeen);
|
||||
if (gotSome) triggerSync();
|
||||
updateStatusIndicators();
|
||||
}
|
||||
}
|
||||
|
||||
public void markItemListAsRead() {
|
||||
FeedUtils.markFeedsRead(fs, null, null, this);
|
||||
Toast.makeText(this, R.string.toast_marked_stories_as_read, Toast.LENGTH_SHORT).show();
|
||||
|
@ -142,29 +117,26 @@ public abstract class ItemsList extends NbActivity implements StateChangedListen
|
|||
}
|
||||
|
||||
// TODO: can all of these be replaced with PrefsUtils queries via FeedSet?
|
||||
protected abstract StoryOrder getStoryOrder();
|
||||
public abstract StoryOrder getStoryOrder();
|
||||
|
||||
protected abstract ReadFilter getReadFilter();
|
||||
|
||||
protected abstract DefaultFeedView getDefaultFeedView();
|
||||
|
||||
@Override
|
||||
public void handleUpdate() {
|
||||
public void handleUpdate(boolean freshData) {
|
||||
updateStatusIndicators();
|
||||
if (itemListFragment != null) {
|
||||
if (freshData && (itemListFragment != null)) {
|
||||
itemListFragment.hasUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStatusIndicators() {
|
||||
boolean isLoading = NBSyncService.isFeedSetSyncing(this.fs);
|
||||
boolean isLoading = NBSyncService.isFeedSetSyncing(this.fs, this);
|
||||
setProgressBarIndeterminateVisibility(isLoading);
|
||||
if (itemListFragment != null) {
|
||||
itemListFragment.setLoading(isLoading);
|
||||
}
|
||||
|
||||
if (overlayStatusText != null) {
|
||||
String syncStatus = NBSyncService.getSyncStatusMessage();
|
||||
String syncStatus = NBSyncService.getSyncStatusMessage(this);
|
||||
if (syncStatus != null) {
|
||||
overlayStatusText.setText(syncStatus);
|
||||
overlayStatusText.setVisibility(View.VISIBLE);
|
||||
|
@ -186,7 +158,6 @@ public abstract class ItemsList extends NbActivity implements StateChangedListen
|
|||
itemListFragment.resetEmptyState();
|
||||
itemListFragment.hasUpdated();
|
||||
itemListFragment.scrollToTop();
|
||||
getFirstStories();
|
||||
}
|
||||
|
||||
public abstract void updateStoryOrderPreference(StoryOrder newValue);
|
||||
|
@ -198,7 +169,6 @@ public abstract class ItemsList extends NbActivity implements StateChangedListen
|
|||
itemListFragment.resetEmptyState();
|
||||
itemListFragment.hasUpdated();
|
||||
itemListFragment.scrollToTop();
|
||||
getFirstStories();
|
||||
}
|
||||
|
||||
protected abstract void updateReadFilterPreference(ReadFilter newValue);
|
||||
|
|
|
@ -73,12 +73,12 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// clear the read-this-session flag from stories so they don't show up in the wrong place
|
||||
NBSyncService.clearPendingStoryRequest();
|
||||
NBSyncService.setActivationMode(NBSyncService.ActivationMode.ALL);
|
||||
FeedUtils.activateAllStories();
|
||||
FeedUtils.clearReadingSession();
|
||||
|
||||
updateStatusIndicators();
|
||||
// this view doesn't show stories, it is safe to perform cleanup
|
||||
NBSyncService.holdStories(false);
|
||||
triggerSync();
|
||||
|
||||
if (PrefsUtils.isLightThemeSelected(this) != isLightTheme) {
|
||||
|
@ -162,9 +162,9 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
}
|
||||
|
||||
@Override
|
||||
public void handleUpdate() {
|
||||
folderFeedList.hasUpdated();
|
||||
public void handleUpdate(boolean freshData) {
|
||||
updateStatusIndicators();
|
||||
if (freshData) folderFeedList.hasUpdated();
|
||||
}
|
||||
|
||||
private void updateStatusIndicators() {
|
||||
|
@ -175,7 +175,7 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
|
|||
}
|
||||
|
||||
if (overlayStatusText != null) {
|
||||
String syncStatus = NBSyncService.getSyncStatusMessage();
|
||||
String syncStatus = NBSyncService.getSyncStatusMessage(this);
|
||||
if (syncStatus != null) {
|
||||
overlayStatusText.setText(syncStatus);
|
||||
overlayStatusText.setVisibility(View.VISIBLE);
|
||||
|
|
|
@ -5,7 +5,6 @@ import android.content.Intent;
|
|||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import com.newsblur.database.BlurDatabaseHelper;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
|
@ -21,8 +20,6 @@ public class NbActivity extends Activity {
|
|||
private final static String UNIQUE_LOGIN_KEY = "uniqueLoginKey";
|
||||
private String uniqueLoginKey;
|
||||
|
||||
protected BlurDatabaseHelper dbHelper;
|
||||
|
||||
/**
|
||||
* Keep track of all activie activities so they can be notified when the sync service
|
||||
* has updated the DB. This is essentially an ultra-lightweight implementation of a
|
||||
|
@ -45,21 +42,8 @@ public class NbActivity extends Activity {
|
|||
uniqueLoginKey = PrefsUtils.getUniqueLoginKey(this);
|
||||
}
|
||||
finishIfNotLoggedIn();
|
||||
|
||||
dbHelper = new BlurDatabaseHelper(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
try {
|
||||
dbHelper.close();
|
||||
} catch (Exception e) {
|
||||
; // Activity is already dead
|
||||
}
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onResume");
|
||||
|
@ -108,14 +92,14 @@ public class NbActivity extends Activity {
|
|||
* Called on each NB activity after the DB has been updated by the sync service. This method
|
||||
* should return as quickly as possible.
|
||||
*/
|
||||
protected void handleUpdate() {
|
||||
protected void handleUpdate(boolean freshData) {
|
||||
Log.w(this.getClass().getName(), "activity doesn't implement handleUpdate");
|
||||
}
|
||||
|
||||
private void _handleUpdate() {
|
||||
private void _handleUpdate(final boolean freshData) {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
handleUpdate();
|
||||
handleUpdate(freshData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -124,14 +108,19 @@ public class NbActivity extends Activity {
|
|||
* Notify all activities in the app that the DB has been updated. Should only be called
|
||||
* by the sync service, which owns updating the DB.
|
||||
*/
|
||||
public static void updateAllActivities() {
|
||||
public static void updateAllActivities(boolean freshData) {
|
||||
synchronized (AllActivities) {
|
||||
for (NbActivity activity : AllActivities) {
|
||||
activity._handleUpdate();
|
||||
activity._handleUpdate(freshData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateAllActivities() {
|
||||
Log.w(NbActivity.class.getName(), "legacy handleUpdate used");
|
||||
NbActivity.updateAllActivities(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of active/foreground NB activities. Used by the sync service to
|
||||
* determine if the app is active so we can honour user requests not to run in
|
||||
|
|
|
@ -5,7 +5,6 @@ import java.util.concurrent.TimeUnit;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
@ -19,6 +18,7 @@ import android.content.Loader;
|
|||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v4.view.ViewPager.OnPageChangeListener;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
@ -45,6 +45,7 @@ import com.newsblur.util.StoryOrder;
|
|||
import com.newsblur.util.StateFilter;
|
||||
import com.newsblur.util.UIUtils;
|
||||
import com.newsblur.util.ViewUtils;
|
||||
import com.newsblur.util.VolumeKeyNavigation;
|
||||
import com.newsblur.view.NonfocusScrollview.ScrollChangeListener;
|
||||
|
||||
public abstract class Reading extends NbActivity implements OnPageChangeListener, OnSeekBarChangeListener, ScrollChangeListener, LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
@ -89,7 +90,6 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
protected Button overlayText, overlaySend;
|
||||
protected FragmentManager fragmentManager;
|
||||
protected ReadingAdapter readingAdapter;
|
||||
protected ContentResolver contentResolver;
|
||||
private boolean stopLoading;
|
||||
protected FeedSet fs;
|
||||
|
||||
|
@ -102,8 +102,9 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
private List<Story> pageHistory;
|
||||
|
||||
protected DefaultFeedView defaultFeedView;
|
||||
private VolumeKeyNavigation volumeKeyNavigation;
|
||||
|
||||
@Override
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceBundle) {
|
||||
super.onCreate(savedInstanceBundle);
|
||||
|
||||
|
@ -133,6 +134,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
currentState = (StateFilter) getIntent().getSerializableExtra(ItemsList.EXTRA_STATE);
|
||||
storyOrder = PrefsUtils.getStoryOrder(this, fs);
|
||||
readFilter = PrefsUtils.getReadFilter(this, fs);
|
||||
volumeKeyNavigation = PrefsUtils.getVolumeKeyNavigation(this);
|
||||
|
||||
if ((savedInstanceBundle != null) && savedInstanceBundle.containsKey(BUNDLE_SELECTED_FEED_VIEW)) {
|
||||
defaultFeedView = (DefaultFeedView)savedInstanceBundle.getSerializable(BUNDLE_SELECTED_FEED_VIEW);
|
||||
|
@ -142,8 +144,6 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
contentResolver = getContentResolver();
|
||||
|
||||
// this value is expensive to compute but doesn't change during a single runtime
|
||||
this.overlayRangeTopPx = (float) UIUtils.convertDPsToPixels(this, OVERLAY_RANGE_TOP_DP);
|
||||
this.overlayRangeBotPx = (float) UIUtils.convertDPsToPixels(this, OVERLAY_RANGE_BOT_DP);
|
||||
|
@ -174,7 +174,6 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
protected void onResume() {
|
||||
super.onResume();
|
||||
// this view shows stories, it is not safe to perform cleanup
|
||||
NBSyncService.holdStories(true);
|
||||
this.stopLoading = false;
|
||||
// onCreate() in our subclass should have called createLoader(), but sometimes the callback never makes it.
|
||||
// this ensures that at least one callback happens after activity re-create.
|
||||
|
@ -183,7 +182,6 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
NBSyncService.holdStories(false);
|
||||
this.stopLoading = true;
|
||||
if (this.unreadSearchLatch != null) {
|
||||
this.unreadSearchLatch.countDown();
|
||||
|
@ -193,7 +191,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) {
|
||||
return dbHelper.getStoriesLoader(fs, currentState);
|
||||
return FeedUtils.dbHelper.getStoriesLoader(fs, currentState);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -211,11 +209,6 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
|
||||
// if this is the first time we've found a cursor, we know the onCreate chain is done
|
||||
if (this.pager == null) {
|
||||
int currentUnreadCount = getUnreadCount();
|
||||
if (currentUnreadCount > this.startingUnreadCount ) {
|
||||
this.startingUnreadCount = currentUnreadCount;
|
||||
}
|
||||
// set up the pager after the unread count, so the first mark-read doesn't happen too quickly
|
||||
setupPager();
|
||||
}
|
||||
|
||||
|
@ -261,7 +254,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
*/
|
||||
private int getUnreadCount() {
|
||||
if (fs.isAllSaved()) return 0; // saved stories doesn't have unreads
|
||||
return dbHelper.getUnreadCount(fs, currentState);
|
||||
return FeedUtils.dbHelper.getUnreadCount(fs, currentState);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -328,9 +321,10 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void handleUpdate() {
|
||||
enableMainProgress(NBSyncService.isFeedSetSyncing(this.fs));
|
||||
updateCursor();
|
||||
protected void handleUpdate(boolean freshData) {
|
||||
enableMainProgress(NBSyncService.isFeedSetSyncing(this.fs, this));
|
||||
updateOverlayNav();
|
||||
if (freshData) updateCursor();
|
||||
}
|
||||
|
||||
private void updateCursor() {
|
||||
|
@ -382,6 +376,10 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
|
||||
@Override
|
||||
public void scrollChanged(int hPos, int vPos, int currentWidth, int currentHeight) {
|
||||
// only update overlay alpha about half the time. modern screens are so dense that it
|
||||
// is way overkill to do it on every pixel
|
||||
if (vPos % 2 == 1) return;
|
||||
|
||||
int scrollMax = currentHeight - contentView.getMeasuredHeight();
|
||||
int posFromBot = (scrollMax - vPos);
|
||||
|
||||
|
@ -432,19 +430,23 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
}
|
||||
|
||||
/**
|
||||
* Update the left/right overlay UI after the read-state of a story changes or we navigate in any way.
|
||||
* Update the next/back overlay UI after the read-state of a story changes or we navigate in any way.
|
||||
*/
|
||||
private void updateOverlayNav() {
|
||||
int currentUnreadCount = getUnreadCount();
|
||||
if (currentUnreadCount > this.startingUnreadCount ) {
|
||||
this.startingUnreadCount = currentUnreadCount;
|
||||
}
|
||||
this.overlayLeft.setEnabled(this.getLastReadPosition(false) != -1);
|
||||
this.overlayRight.setText((getUnreadCount() > 0) ? R.string.overlay_next : R.string.overlay_done);
|
||||
this.overlayRight.setBackgroundResource((getUnreadCount() > 0) ? R.drawable.selector_overlay_bg_right : R.drawable.selector_overlay_bg_right_done);
|
||||
this.overlayRight.setText((currentUnreadCount > 0) ? R.string.overlay_next : R.string.overlay_done);
|
||||
this.overlayRight.setBackgroundResource((currentUnreadCount > 0) ? R.drawable.selector_overlay_bg_right : R.drawable.selector_overlay_bg_right_done);
|
||||
|
||||
if (this.startingUnreadCount == 0 ) {
|
||||
// sessions with no unreads just show a full progress bar
|
||||
this.overlayProgress.setMax(1);
|
||||
this.overlayProgress.setProgress(1);
|
||||
} else {
|
||||
int unreadProgress = this.startingUnreadCount - getUnreadCount();
|
||||
int unreadProgress = this.startingUnreadCount - currentUnreadCount;
|
||||
this.overlayProgress.setMax(this.startingUnreadCount);
|
||||
this.overlayProgress.setProgress(unreadProgress);
|
||||
}
|
||||
|
@ -490,13 +492,17 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
* load is not needed and all latches are tripped.
|
||||
*/
|
||||
private void checkStoryCount(int position) {
|
||||
if (AppConstants.VERBOSE_LOG) {
|
||||
Log.d(this.getClass().getName(), String.format("story %d of %d selected, stopLoad: %b", position, stories.getCount(), stopLoading));
|
||||
}
|
||||
// if the pager is at or near the number of stories loaded, check for more unless we know we are at the end of the list
|
||||
if ((position + AppConstants.READING_STORY_PRELOAD) >= stories.getCount()) {
|
||||
if (stories == null ) {
|
||||
triggerRefresh(position + AppConstants.READING_STORY_PRELOAD);
|
||||
}
|
||||
} else {
|
||||
if (AppConstants.VERBOSE_LOG) {
|
||||
Log.d(this.getClass().getName(), String.format("story %d of %d selected, stopLoad: %b", position, stories.getCount(), stopLoading));
|
||||
}
|
||||
// if the pager is at or near the number of stories loaded, check for more unless we know we are at the end of the list
|
||||
if ((position + AppConstants.READING_STORY_PRELOAD) >= stories.getCount()) {
|
||||
triggerRefresh(position + AppConstants.READING_STORY_PRELOAD);
|
||||
}
|
||||
}
|
||||
|
||||
if (stopLoading) {
|
||||
// if we terminated because we are well and truly done, break any search loops and stop progress indication
|
||||
|
@ -719,4 +725,37 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
|
|||
return readingAdapter.getExistingItem(pager.getCurrentItem());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (isVolumeKeyNavigationEvent(keyCode)) {
|
||||
processVolumeKeyNavigationEvent(keyCode);
|
||||
return true;
|
||||
} else {
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isVolumeKeyNavigationEvent(int keyCode) {
|
||||
return volumeKeyNavigation != VolumeKeyNavigation.OFF &&
|
||||
(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP);
|
||||
}
|
||||
|
||||
private void processVolumeKeyNavigationEvent(int keyCode) {
|
||||
if ((keyCode == KeyEvent.KEYCODE_VOLUME_DOWN && volumeKeyNavigation == VolumeKeyNavigation.DOWN_NEXT) ||
|
||||
(keyCode == KeyEvent.KEYCODE_VOLUME_UP && volumeKeyNavigation == VolumeKeyNavigation.UP_NEXT)) {
|
||||
overlayRight(overlayRight);
|
||||
} else {
|
||||
overlayLeft(overlayLeft);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
// Required to prevent the default sound playing when the volume key is pressed
|
||||
if (isVolumeKeyNavigationEvent(keyCode)) {
|
||||
return true;
|
||||
} else {
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ public class SavedStoriesItemsList extends ItemsList {
|
|||
// relevant since saved stories have no read/unread status nor ordering.
|
||||
|
||||
@Override
|
||||
protected StoryOrder getStoryOrder() {
|
||||
public StoryOrder getStoryOrder() {
|
||||
return PrefsUtils.getStoryOrderForFolder(this, PrefConstants.ALL_STORIES_FOLDER_NAME);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,11 @@ package com.newsblur.activity;
|
|||
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.content.CursorLoader;
|
||||
import android.content.Loader;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.database.MixedFeedsReadingAdapter;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
|
||||
public class SavedStoriesReading extends Reading {
|
||||
|
||||
|
@ -18,7 +15,7 @@ public class SavedStoriesReading extends Reading {
|
|||
super.onCreate(savedInstanceBundle);
|
||||
|
||||
setTitle(getResources().getString(R.string.saved_stories_title));
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), getContentResolver(), defaultFeedView, null);
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), defaultFeedView, null);
|
||||
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
@ -26,7 +23,7 @@ public class SavedStoriesReading extends Reading {
|
|||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
||||
// every time we see a set of saved stories, tag them so they don't disappear during this reading session
|
||||
dbHelper.markSavedReadingSession();
|
||||
FeedUtils.dbHelper.markSavedReadingSession();
|
||||
super.onLoadFinished(loader, cursor);
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ public class SocialFeedItemsList extends ItemsList {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected StoryOrder getStoryOrder() {
|
||||
public StoryOrder getStoryOrder() {
|
||||
return PrefsUtils.getStoryOrderForFeed(this, socialFeed.userId);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,8 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.content.CursorLoader;
|
||||
import android.content.Loader;
|
||||
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.database.MixedFeedsReadingAdapter;
|
||||
import com.newsblur.domain.SocialFeed;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
|
||||
public class SocialFeedReading extends Reading {
|
||||
|
||||
|
@ -26,7 +18,7 @@ public class SocialFeedReading extends Reading {
|
|||
|
||||
setTitle(getIntent().getStringExtra(EXTRA_USERNAME));
|
||||
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), getContentResolver(), defaultFeedView, userId);
|
||||
readingAdapter = new MixedFeedsReadingAdapter(getFragmentManager(), defaultFeedView, userId);
|
||||
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import com.newsblur.domain.SocialFeed;
|
|||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.domain.UserProfile;
|
||||
import com.newsblur.network.domain.StoriesResponse;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.FeedSet;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
|
@ -138,6 +139,10 @@ public class BlurDatabaseHelper {
|
|||
synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.FEED_FOLDER_MAP_TABLE, null, null);}
|
||||
}
|
||||
|
||||
public void vacuum() {
|
||||
synchronized (RW_MUTEX) {dbRW.execSQL("VACUUM");}
|
||||
}
|
||||
|
||||
public void deleteFeed(String feedId) {
|
||||
String[] selArgs = new String[] {feedId};
|
||||
synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.FEED_TABLE, DatabaseConstants.FEED_ID + " = ?", selArgs);}
|
||||
|
@ -204,7 +209,19 @@ public class BlurDatabaseHelper {
|
|||
return hashes;
|
||||
}
|
||||
|
||||
public void insertStories(StoriesResponse apiResponse) {
|
||||
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());
|
||||
while (c.moveToNext()) {
|
||||
for (String url : TextUtils.split(c.getString(c.getColumnIndexOrThrow(DatabaseConstants.STORY_IMAGE_URLS)), ",")) {
|
||||
urls.add(url);
|
||||
}
|
||||
}
|
||||
c.close();
|
||||
return urls;
|
||||
}
|
||||
|
||||
public void insertStories(StoriesResponse apiResponse, NBSyncService.ActivationMode actMode, long modeCutoff) {
|
||||
// to insert classifiers, we need to determine the feed ID of the stories in this
|
||||
// response, so sniff one out.
|
||||
String impliedFeedId = null;
|
||||
|
@ -243,8 +260,35 @@ public class BlurDatabaseHelper {
|
|||
}
|
||||
impliedFeedId = story.feedId;
|
||||
}
|
||||
bulkInsertValues(DatabaseConstants.STORY_TABLE, storyValues);
|
||||
bulkInsertValues(DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE, socialStoryValues);
|
||||
// we don't use bulkInsertValues for stories, since we need to put markStoriesActive within the transaction
|
||||
if (storyValues.size() > 0) {
|
||||
synchronized (RW_MUTEX) {
|
||||
dbRW.beginTransaction();
|
||||
try {
|
||||
for(ContentValues values: storyValues) {
|
||||
dbRW.insertWithOnConflict(DatabaseConstants.STORY_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
markStoriesActive(actMode, modeCutoff);
|
||||
dbRW.setTransactionSuccessful();
|
||||
} finally {
|
||||
dbRW.endTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (socialStoryValues.size() > 0) {
|
||||
synchronized (RW_MUTEX) {
|
||||
dbRW.beginTransaction();
|
||||
try {
|
||||
for(ContentValues values: socialStoryValues) {
|
||||
dbRW.insertWithOnConflict(DatabaseConstants.SOCIALFEED_STORY_MAP_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
}
|
||||
markStoriesActive(actMode, modeCutoff);
|
||||
dbRW.setTransactionSuccessful();
|
||||
} finally {
|
||||
dbRW.endTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle classifiers
|
||||
if (apiResponse.classifiers != null) {
|
||||
|
@ -495,6 +539,22 @@ public class BlurDatabaseHelper {
|
|||
synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, null, null);}
|
||||
}
|
||||
|
||||
public void markStoriesActive(NBSyncService.ActivationMode actMode, long modeCutoff) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(DatabaseConstants.STORY_ACTIVE, true);
|
||||
|
||||
String selection = null;
|
||||
if (actMode == NBSyncService.ActivationMode.ALL) {
|
||||
// leave the selection null to mark all
|
||||
} else if (actMode == NBSyncService.ActivationMode.OLDER) {
|
||||
selection = DatabaseConstants.STORY_TIMESTAMP + " <= " + Long.toString(modeCutoff);
|
||||
} else if (actMode == NBSyncService.ActivationMode.NEWER) {
|
||||
selection = DatabaseConstants.STORY_TIMESTAMP + " >= " + Long.toString(modeCutoff);
|
||||
}
|
||||
|
||||
synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.STORY_TABLE, values, selection, null);}
|
||||
}
|
||||
|
||||
public Loader<Cursor> getSocialFeedsLoader(final StateFilter stateFilter) {
|
||||
return new QueryCursorLoader(context) {
|
||||
protected Cursor createCursor() {return getSocialFeedsCursor(stateFilter, cancellationSignal);}
|
||||
|
@ -542,12 +602,14 @@ public class BlurDatabaseHelper {
|
|||
}
|
||||
|
||||
public Cursor getStoriesCursor(FeedSet fs, StateFilter stateFilter, CancellationSignal cancellationSignal) {
|
||||
if (fs == null) return null;
|
||||
ReadFilter readFilter = PrefsUtils.getReadFilter(context, fs);
|
||||
StoryOrder order = PrefsUtils.getStoryOrder(context, fs);
|
||||
return getStoriesCursor(fs, stateFilter, readFilter, order, cancellationSignal);
|
||||
}
|
||||
|
||||
private Cursor getStoriesCursor(FeedSet fs, StateFilter stateFilter, ReadFilter readFilter, StoryOrder order, CancellationSignal cancellationSignal) {
|
||||
if (fs == null) return null;
|
||||
|
||||
if (fs.getSingleFeed() != null) {
|
||||
|
||||
|
@ -611,6 +673,67 @@ public class BlurDatabaseHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public void clearClassifiersForFeed(String feedId) {
|
||||
String[] selArgs = new String[] {feedId};
|
||||
synchronized (RW_MUTEX) {dbRW.delete(DatabaseConstants.CLASSIFIER_TABLE, DatabaseConstants.CLASSIFIER_ID + " = ?", selArgs);}
|
||||
}
|
||||
|
||||
public void insertClassifier(Classifier classifier) {
|
||||
bulkInsertValues(DatabaseConstants.CLASSIFIER_TABLE, classifier.getContentValues());
|
||||
}
|
||||
|
||||
public Classifier getClassifierForFeed(String feedId) {
|
||||
String[] selArgs = new String[] {feedId};
|
||||
Cursor c = dbRO.query(DatabaseConstants.CLASSIFIER_TABLE, null, DatabaseConstants.CLASSIFIER_ID + " = ?", selArgs, null, null, null);
|
||||
Classifier classifier = Classifier.fromCursor(c);
|
||||
closeQuietly(c);
|
||||
return classifier;
|
||||
}
|
||||
|
||||
public List<Comment> getComments(String storyId) {
|
||||
String[] selArgs = new String[] {storyId};
|
||||
String selection = DatabaseConstants.COMMENT_STORYID + " = ?";
|
||||
Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE, DatabaseConstants.COMMENT_COLUMNS, selection, selArgs, null, null, null);
|
||||
List<Comment> comments = new ArrayList<Comment>(c.getCount());
|
||||
while (c.moveToNext()) {
|
||||
comments.add(Comment.fromCursor(c));
|
||||
}
|
||||
closeQuietly(c);
|
||||
return comments;
|
||||
}
|
||||
|
||||
public Comment getComment(String storyId, String userId) {
|
||||
String selection = DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?";
|
||||
String[] selArgs = new String[] {storyId, userId};
|
||||
Cursor c = dbRO.query(DatabaseConstants.COMMENT_TABLE, DatabaseConstants.COMMENT_COLUMNS, selection, selArgs, null, null, null);
|
||||
if (c.getCount() < 1) return null;
|
||||
c.moveToFirst();
|
||||
Comment comment = Comment.fromCursor(c);
|
||||
closeQuietly(c);
|
||||
return comment;
|
||||
}
|
||||
|
||||
public UserProfile getUserProfile(String userId) {
|
||||
String[] selArgs = new String[] {userId};
|
||||
String selection = DatabaseConstants.USER_USERID + " = ?";
|
||||
Cursor c = dbRO.query(DatabaseConstants.USER_TABLE, null, selection, selArgs, null, null, null);
|
||||
UserProfile profile = UserProfile.fromCursor(c);
|
||||
closeQuietly(c);
|
||||
return profile;
|
||||
}
|
||||
|
||||
public List<Reply> getCommentReplies(String commentId) {
|
||||
String[] selArgs = new String[] {commentId};
|
||||
String selection = DatabaseConstants.REPLY_COMMENTID+ " = ?";
|
||||
Cursor c = dbRO.query(DatabaseConstants.REPLY_TABLE, DatabaseConstants.REPLY_COLUMNS, selection, selArgs, null, null, DatabaseConstants.REPLY_DATE + " DESC");
|
||||
List<Reply> replies = new ArrayList<Reply>(c.getCount());
|
||||
while (c.moveToNext()) {
|
||||
replies.add(Reply.fromCursor(c));
|
||||
}
|
||||
closeQuietly(c);
|
||||
return replies;
|
||||
}
|
||||
|
||||
public static void closeQuietly(Cursor c) {
|
||||
if (c == null) return;
|
||||
try {c.close();} catch (Exception e) {;}
|
||||
|
|
|
@ -97,6 +97,8 @@ public class DatabaseConstants {
|
|||
public static final String STORY_SOURCE_USER_ID = "sourceUserId";
|
||||
public static final String STORY_TAGS = "tags";
|
||||
public static final String STORY_HASH = "story_hash";
|
||||
public static final String STORY_ACTIVE = "active";
|
||||
public static final String STORY_IMAGE_URLS = "image_urls";
|
||||
|
||||
public static final String STORY_TEXT_TABLE = "storytext";
|
||||
public static final String STORY_TEXT_STORY_HASH = "story_hash";
|
||||
|
@ -226,7 +228,9 @@ public class DatabaseConstants {
|
|||
STORY_READ_THIS_SESSION + INTEGER + ", " +
|
||||
STORY_STARRED + INTEGER + ", " +
|
||||
STORY_STARRED_DATE + INTEGER + ", " +
|
||||
STORY_TITLE + TEXT +
|
||||
STORY_TITLE + TEXT + ", " +
|
||||
STORY_ACTIVE + INTEGER + " DEFAULT 0, " +
|
||||
STORY_IMAGE_URLS + TEXT +
|
||||
")";
|
||||
|
||||
static final String STORY_TEXT_SQL = "CREATE TABLE " + STORY_TEXT_TABLE + " (" +
|
||||
|
@ -307,7 +311,8 @@ public class DatabaseConstants {
|
|||
STORY_AUTHORS, STORY_COMMENT_COUNT, STORY_CONTENT, STORY_SHORT_CONTENT, STORY_TIMESTAMP, STORY_SHARED_DATE, STORY_SHORTDATE, STORY_LONGDATE,
|
||||
STORY_TABLE + "." + STORY_FEED_ID, STORY_TABLE + "." + STORY_ID, STORY_INTELLIGENCE_AUTHORS, STORY_INTELLIGENCE_FEED, STORY_INTELLIGENCE_TAGS,
|
||||
STORY_INTELLIGENCE_TITLE, STORY_PERMALINK, STORY_READ, STORY_STARRED, STORY_STARRED_DATE, STORY_SHARE_COUNT, STORY_TAGS, STORY_TITLE,
|
||||
STORY_SOCIAL_USER_ID, STORY_SOURCE_USER_ID, STORY_SHARED_USER_IDS, STORY_FRIEND_USER_IDS, STORY_PUBLIC_USER_IDS, STORY_SUM_TOTAL, STORY_HASH
|
||||
STORY_SOCIAL_USER_ID, STORY_SOURCE_USER_ID, STORY_SHARED_USER_IDS, STORY_FRIEND_USER_IDS, STORY_PUBLIC_USER_IDS, STORY_SUM_TOTAL, STORY_HASH,
|
||||
STORY_IMAGE_URLS
|
||||
};
|
||||
|
||||
public static final String MULTIFEED_STORIES_QUERY_BASE =
|
||||
|
@ -340,6 +345,8 @@ public class DatabaseConstants {
|
|||
if (stateSelection != null) {
|
||||
q.append(" AND " + stateSelection);
|
||||
}
|
||||
|
||||
q.append(" AND (" + STORY_TABLE + "." + STORY_ACTIVE + " = 1)");
|
||||
|
||||
if (dedupCol != null) {
|
||||
q.append( " GROUP BY " + dedupCol);
|
||||
|
@ -403,10 +410,12 @@ public class DatabaseConstants {
|
|||
}
|
||||
|
||||
public static String getStorySortOrder(StoryOrder storyOrder) {
|
||||
// it is not uncommon for a feed to have multiple stories with exactly the same timestamp. we
|
||||
// arbitrarily pick a second sort column so sortation is stable.
|
||||
if (storyOrder == StoryOrder.NEWEST) {
|
||||
return STORY_TIMESTAMP + " DESC";
|
||||
return STORY_TIMESTAMP + " DESC, " + STORY_HASH + " DESC";
|
||||
} else {
|
||||
return STORY_TIMESTAMP + " ASC";
|
||||
return STORY_TIMESTAMP + " ASC, " + STORY_HASH + " ASC";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,191 +0,0 @@
|
|||
package com.newsblur.database;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import android.R.string;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.newsblur.util.AppConstants;
|
||||
|
||||
/**
|
||||
* A magic subclass of ContentProvider that enhances calls to the DB for presumably more simple caller syntax.
|
||||
*
|
||||
* TODO: GET RID OF THIS CLASS. Per the docs for ContentProfider, one is not required
|
||||
* or recommended for DB access unless sharing data outside of the app, which we do
|
||||
* not. All DB ops should be done via BlurDatabaseHelper using straightforward,
|
||||
* standard SQL.
|
||||
*/
|
||||
public class FeedProvider extends ContentProvider {
|
||||
|
||||
public static final String AUTHORITY = "com.newsblur";
|
||||
public static final String VERSION = "v1";
|
||||
|
||||
public static final Uri CLASSIFIER_URI = Uri.parse("content://" + AUTHORITY + "/" + VERSION + "/classifiers/");
|
||||
public static final Uri USERS_URI = Uri.parse("content://" + AUTHORITY + "/" + VERSION + "/users/");
|
||||
public static final Uri COMMENTS_URI = Uri.parse("content://" + AUTHORITY + "/" + VERSION + "/comments/");
|
||||
public static final Uri REPLIES_URI = Uri.parse("content://" + AUTHORITY + "/" + VERSION + "/replies/");
|
||||
|
||||
private static final int STORY_COMMENTS = 9;
|
||||
private static final int REPLIES = 15;
|
||||
private static final int CLASSIFIERS_FOR_FEED = 19;
|
||||
private static final int USERS = 21;
|
||||
|
||||
private BlurDatabase databaseHelper;
|
||||
|
||||
private static UriMatcher uriMatcher;
|
||||
static {
|
||||
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
uriMatcher.addURI(AUTHORITY, VERSION + "/classifiers/#/", CLASSIFIERS_FOR_FEED);
|
||||
uriMatcher.addURI(AUTHORITY, VERSION + "/comments/", STORY_COMMENTS);
|
||||
uriMatcher.addURI(AUTHORITY, VERSION + "/replies/", REPLIES);
|
||||
uriMatcher.addURI(AUTHORITY, VERSION + "/users/", USERS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
synchronized (BlurDatabaseHelper.RW_MUTEX) {
|
||||
final SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
switch (uriMatcher.match(uri)) {
|
||||
case CLASSIFIERS_FOR_FEED:
|
||||
return db.delete(DatabaseConstants.CLASSIFIER_TABLE, DatabaseConstants.CLASSIFIER_ID + " = ?", new String[] { uri.getLastPathSegment() });
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
synchronized (BlurDatabaseHelper.RW_MUTEX) {
|
||||
final SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
Uri resultUri = null;
|
||||
switch (uriMatcher.match(uri)) {
|
||||
|
||||
case USERS:
|
||||
db.insertWithOnConflict(DatabaseConstants.USER_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
resultUri = uri.buildUpon().appendPath(values.getAsString(DatabaseConstants.USER_USERID)).build();
|
||||
break;
|
||||
|
||||
// Inserting a classifier for a feed
|
||||
case CLASSIFIERS_FOR_FEED:
|
||||
values.put(DatabaseConstants.CLASSIFIER_ID, uri.getLastPathSegment());
|
||||
db.insertWithOnConflict(DatabaseConstants.CLASSIFIER_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
break;
|
||||
|
||||
// Inserting a comment
|
||||
case STORY_COMMENTS:
|
||||
db.insertWithOnConflict(DatabaseConstants.COMMENT_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
break;
|
||||
|
||||
// Inserting a reply
|
||||
case REPLIES:
|
||||
db.insertWithOnConflict(DatabaseConstants.REPLY_TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||
break;
|
||||
|
||||
case UriMatcher.NO_MATCH:
|
||||
Log.e(this.getClass().getName(), "No match found for URI: " + uri.toString());
|
||||
break;
|
||||
}
|
||||
return resultUri;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
synchronized (BlurDatabaseHelper.RW_MUTEX) {
|
||||
databaseHelper = new BlurDatabase(getContext().getApplicationContext());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple utility wrapper that lets us log the insanely complex queries used below for debugging.
|
||||
*/
|
||||
class LoggingDatabase {
|
||||
SQLiteDatabase mdb;
|
||||
public LoggingDatabase(SQLiteDatabase db) {
|
||||
mdb = db;
|
||||
}
|
||||
public Cursor rawQuery(String sql, String[] selectionArgs) {
|
||||
if (AppConstants.VERBOSE_LOG_DB) {
|
||||
Log.d(LoggingDatabase.class.getName(), "rawQuery: " + sql);
|
||||
Log.d(LoggingDatabase.class.getName(), "selArgs : " + Arrays.toString(selectionArgs));
|
||||
}
|
||||
Cursor cursor = mdb.rawQuery(sql, selectionArgs);
|
||||
if (AppConstants.VERBOSE_LOG_DB) {
|
||||
Log.d(LoggingDatabase.class.getName(), "result rows: " + cursor.getCount());
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) {
|
||||
if (AppConstants.VERBOSE_LOG_DB) {
|
||||
Log.d(LoggingDatabase.class.getName(), "selection: " + selection);
|
||||
}
|
||||
return mdb.query(table, columns, selection, selectionArgs, groupBy, having, orderBy);
|
||||
}
|
||||
public void execSQL(String sql) {
|
||||
if (AppConstants.VERBOSE_LOG_DB) {
|
||||
Log.d(LoggingDatabase.class.getName(), "execSQL: " + sql);
|
||||
}
|
||||
mdb.execSQL(sql);
|
||||
}
|
||||
public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
|
||||
return mdb.update(table, values, whereClause, whereArgs);
|
||||
}
|
||||
public long insertWithOnConflict(String table, String nullColumnHack, ContentValues initialValues, int conflictAlgorithm) {
|
||||
return mdb.insertWithOnConflict(table, nullColumnHack, initialValues, conflictAlgorithm);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
|
||||
final SQLiteDatabase rdb = databaseHelper.getReadableDatabase();
|
||||
final LoggingDatabase db = new LoggingDatabase(rdb);
|
||||
switch (uriMatcher.match(uri)) {
|
||||
|
||||
case USERS:
|
||||
return db.query(DatabaseConstants.USER_TABLE, projection, selection, selectionArgs, null, null, null);
|
||||
|
||||
// Query for classifiers for a given feed
|
||||
case CLASSIFIERS_FOR_FEED:
|
||||
return db.query(DatabaseConstants.CLASSIFIER_TABLE, null, DatabaseConstants.CLASSIFIER_ID + " = ?", new String[] { uri.getLastPathSegment() }, null, null, null);
|
||||
|
||||
// Querying for a stories from a feed
|
||||
case STORY_COMMENTS:
|
||||
if (selectionArgs.length == 1) {
|
||||
selection = DatabaseConstants.COMMENT_STORYID + " = ?";
|
||||
} else {
|
||||
selection = DatabaseConstants.COMMENT_STORYID + " = ? AND " + DatabaseConstants.COMMENT_USERID + " = ?";
|
||||
}
|
||||
return db.query(DatabaseConstants.COMMENT_TABLE, DatabaseConstants.COMMENT_COLUMNS, selection, selectionArgs, null, null, null);
|
||||
|
||||
// Querying for replies to a comment
|
||||
case REPLIES:
|
||||
selection = DatabaseConstants.REPLY_COMMENTID+ " = ?";
|
||||
return db.query(DatabaseConstants.REPLY_TABLE, DatabaseConstants.REPLY_COLUMNS, selection, selectionArgs, null, null, null);
|
||||
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unknown URI: " + uri);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
throw new UnsupportedOperationException("Unknown URI: " + uri);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,8 +1,5 @@
|
|||
package com.newsblur.database;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentManager;
|
||||
|
||||
|
@ -11,14 +8,12 @@ import com.newsblur.domain.Classifier;
|
|||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.fragment.ReadingItemFragment;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
|
||||
public class MixedFeedsReadingAdapter extends ReadingAdapter {
|
||||
|
||||
private final ContentResolver resolver;
|
||||
|
||||
public MixedFeedsReadingAdapter(final FragmentManager fragmentManager, final ContentResolver resolver, DefaultFeedView defaultFeedView, String sourceUserId) {
|
||||
public MixedFeedsReadingAdapter(FragmentManager fragmentManager, DefaultFeedView defaultFeedView, String sourceUserId) {
|
||||
super(fragmentManager, defaultFeedView, sourceUserId);
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -30,9 +25,9 @@ public class MixedFeedsReadingAdapter extends ReadingAdapter {
|
|||
String feedFaviconText = stories.getString(stories.getColumnIndex(DatabaseConstants.FEED_FAVICON_TEXT));
|
||||
String feedFaviconUrl = stories.getString(stories.getColumnIndex(DatabaseConstants.FEED_FAVICON_URL));
|
||||
|
||||
Uri classifierUri = FeedProvider.CLASSIFIER_URI.buildUpon().appendPath(story.feedId).build();
|
||||
Cursor feedClassifierCursor = resolver.query(classifierUri, null, null, null, null);
|
||||
Classifier classifier = Classifier.fromCursor(feedClassifierCursor);
|
||||
// TODO: does the pager generate new fragments in the UI thread? If so, classifiers should
|
||||
// be loaded async by the fragment itself
|
||||
Classifier classifier = FeedUtils.dbHelper.getClassifierForFeed(story.feedId);
|
||||
|
||||
return ReadingItemFragment.newInstance(story, feedTitle, feedFaviconColor, feedFaviconFade, feedFaviconBorder, feedFaviconText, feedFaviconUrl, classifier, true, defaultFeedView, sourceUserId);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,9 @@ import android.database.Cursor;
|
|||
import android.os.CancellationSignal;
|
||||
import android.os.Build;
|
||||
import android.os.OperationCanceledException;
|
||||
import android.util.Log;
|
||||
|
||||
import com.newsblur.util.AppConstants;
|
||||
|
||||
/**
|
||||
* A partial copy of android.content.CursorLoader with the bits related to ContentProviders
|
||||
|
@ -34,9 +37,17 @@ public abstract class QueryCursorLoader extends AsyncTaskLoader<Cursor> {
|
|||
}
|
||||
}
|
||||
try {
|
||||
long startTime = System.nanoTime();
|
||||
int count = -1;
|
||||
Cursor c = createCursor();
|
||||
if (c != null) {
|
||||
c.getCount();
|
||||
// this call to getCount is *not* just for the instrumentation, it ensures the cursor is fully ready before
|
||||
// being called back. if the instrumentation is ever removed, do not remove this call.
|
||||
count = c.getCount();
|
||||
}
|
||||
if (AppConstants.VERBOSE_LOG_DB) {
|
||||
long time = System.nanoTime() - startTime;
|
||||
Log.d(this.getClass().getName(), "cursor load: " + (time/1000000L) + "ms to load " + count + " rows");
|
||||
}
|
||||
return c;
|
||||
} finally {
|
||||
|
|
|
@ -121,6 +121,7 @@ public class Story implements Serializable {
|
|||
values.put(DatabaseConstants.STORY_STARRED_DATE, starredTimestamp);
|
||||
values.put(DatabaseConstants.STORY_FEED_ID, feedId);
|
||||
values.put(DatabaseConstants.STORY_HASH, storyHash);
|
||||
values.put(DatabaseConstants.STORY_IMAGE_URLS, TextUtils.join(",", imageUrls));
|
||||
return values;
|
||||
}
|
||||
|
||||
|
@ -155,6 +156,7 @@ public class Story implements Serializable {
|
|||
story.feedId = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_FEED_ID));
|
||||
story.id = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_ID));
|
||||
story.storyHash = cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_HASH));
|
||||
story.imageUrls = TextUtils.split(cursor.getString(cursor.getColumnIndex(DatabaseConstants.STORY_IMAGE_URLS)), ",");
|
||||
return story;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import android.widget.Toast;
|
|||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.network.APIManager;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
|
||||
public class AddFeedFragment extends DialogFragment {
|
||||
|
||||
|
@ -50,6 +51,8 @@ public class AddFeedFragment extends DialogFragment {
|
|||
protected void onPostExecute(Boolean result) {
|
||||
if (result) {
|
||||
activity.finish();
|
||||
// trigger a sync when we return to Main so that the new feed will show up
|
||||
NBSyncService.forceFeedsFolders();
|
||||
AddFeedFragment.this.dismiss();
|
||||
} else {
|
||||
AddFeedFragment.this.dismiss();
|
||||
|
|
|
@ -31,7 +31,7 @@ public class DeleteFeedFragment extends DialogFragment {
|
|||
Bundle args = new Bundle();
|
||||
args.putString(FEED_ID, feed.feedId);
|
||||
args.putString(FEED_NAME, feed.title);
|
||||
args.putString(FOLDER_NAME, folderName);
|
||||
args.putString(FOLDER_NAME, parseFolderName(folderName));
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
@ -41,11 +41,25 @@ public class DeleteFeedFragment extends DialogFragment {
|
|||
Bundle args = new Bundle();
|
||||
args.putString(FEED_ID, feed.userId);
|
||||
args.putString(FEED_NAME, feed.feedTitle);
|
||||
args.putString(FOLDER_NAME, folderName);
|
||||
args.putString(FOLDER_NAME, parseFolderName(folderName));
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nested folders are named "parent" - "child" so we need to find the last "-"
|
||||
* and pull out the child folder name for the delete to succeed.
|
||||
*/
|
||||
private static String parseFolderName(String folderName) {
|
||||
int index = folderName.lastIndexOf("-");
|
||||
if (index == -1) {
|
||||
return folderName;
|
||||
} else {
|
||||
// + 2 to ignore - and the first whitespace
|
||||
return folderName.substring(index + 2);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
|
@ -58,7 +72,7 @@ public class DeleteFeedFragment extends DialogFragment {
|
|||
// called from the feed view so finish
|
||||
Activity activity = DeleteFeedFragment.this.getActivity();
|
||||
if (activity instanceof Main) {
|
||||
((Main)activity).handleUpdate();
|
||||
((Main)activity).handleUpdate(true);
|
||||
} else {
|
||||
activity.finish();
|
||||
}
|
||||
|
|
|
@ -76,13 +76,13 @@ public class FolderListFragment extends NbFragment implements OnGroupClickListen
|
|||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
switch (id) {
|
||||
case SOCIALFEEDS_LOADER:
|
||||
return dbHelper.getSocialFeedsLoader(currentState);
|
||||
return FeedUtils.dbHelper.getSocialFeedsLoader(currentState);
|
||||
case FOLDERFEEDMAP_LOADER:
|
||||
return dbHelper.getFolderFeedMapLoader();
|
||||
return FeedUtils.dbHelper.getFolderFeedMapLoader();
|
||||
case FEEDS_LOADER:
|
||||
return dbHelper.getFeedsLoader(currentState);
|
||||
return FeedUtils.dbHelper.getFeedsLoader(currentState);
|
||||
case SAVEDCOUNT_LOADER:
|
||||
return dbHelper.getSavedStoryCountLoader();
|
||||
return FeedUtils.dbHelper.getSavedStoryCountLoader();
|
||||
default:
|
||||
throw new IllegalArgumentException("unknown loader created");
|
||||
}
|
||||
|
@ -90,6 +90,7 @@ public class FolderListFragment extends NbFragment implements OnGroupClickListen
|
|||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
||||
if (cursor == null) return;
|
||||
try {
|
||||
switch (loader.getId()) {
|
||||
case SOCIALFEEDS_LOADER:
|
||||
|
|
|
@ -26,8 +26,10 @@ import android.widget.TextView;
|
|||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.activity.ItemsList;
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.database.StoryItemsAdapter;
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.FeedSet;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
|
@ -44,8 +46,8 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
|
|||
protected StoryItemsAdapter adapter;
|
||||
protected DefaultFeedView defaultFeedView;
|
||||
protected StateFilter currentState;
|
||||
private boolean isLoading = true;
|
||||
private boolean cursorSeenYet = false;
|
||||
private boolean firstStorySeenYet = false;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -80,16 +82,17 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
|
|||
}
|
||||
}
|
||||
|
||||
private void triggerRefresh(int desiredStoryCount, int totalSeen) {
|
||||
boolean gotSome = NBSyncService.requestMoreForFeed(getFeedSet(), desiredStoryCount, totalSeen);
|
||||
if (gotSome) triggerSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the DB was cleared.
|
||||
*/
|
||||
public void resetEmptyState() {
|
||||
cursorSeenYet = false;
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
public void setLoading(boolean loading) {
|
||||
isLoading = loading;
|
||||
firstStorySeenYet = false;
|
||||
}
|
||||
|
||||
private void updateLoadingIndicator() {
|
||||
|
@ -103,6 +106,7 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
|
|||
}
|
||||
TextView emptyView = (TextView) itemList.getEmptyView();
|
||||
|
||||
boolean isLoading = NBSyncService.isFeedSetSyncing(getFeedSet(), activity);
|
||||
if (isLoading || (!cursorSeenYet)) {
|
||||
emptyView.setText(R.string.empty_list_view_loading);
|
||||
} else {
|
||||
|
@ -125,13 +129,9 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
|
|||
|
||||
@Override
|
||||
public synchronized void onScroll(AbsListView view, int firstVisible, int visibleCount, int totalCount) {
|
||||
// if we have seen a cursor, this method means the list was updated or scrolled. now is a good
|
||||
// time to see if we need more stories
|
||||
if (cursorSeenYet) {
|
||||
// load an extra page or two worth of stories past the viewport
|
||||
int desiredStoryCount = firstVisible + (visibleCount*2) + 1;
|
||||
activity.triggerRefresh(desiredStoryCount, totalCount);
|
||||
}
|
||||
// load an extra page or two worth of stories past the viewport
|
||||
int desiredStoryCount = firstVisible + (visibleCount*2) + 1;
|
||||
triggerRefresh(desiredStoryCount, totalCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -154,24 +154,38 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis
|
|||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
|
||||
return dbHelper.getStoriesLoader(getFeedSet(), currentState);
|
||||
return FeedUtils.dbHelper.getStoriesLoader(getFeedSet(), currentState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
||||
public synchronized void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
||||
if (cursor != null) {
|
||||
cursorSeenYet = true;
|
||||
if (cursor.getCount() == 0) {
|
||||
activity.triggerRefresh(1, 0);
|
||||
if (cursor.getCount() < 1) {
|
||||
triggerRefresh(1, 0);
|
||||
} else {
|
||||
if (!firstStorySeenYet) {
|
||||
// once we have at least a single story, we can instruct the sync service as to how to safely
|
||||
// activate new stories we recieve
|
||||
firstStorySeenYet = true;
|
||||
cursor.moveToFirst();
|
||||
long cutoff = cursor.getLong(cursor.getColumnIndex(DatabaseConstants.STORY_TIMESTAMP));
|
||||
cursor.moveToPosition(-1);
|
||||
if (activity.getStoryOrder() == StoryOrder.NEWEST) {
|
||||
NBSyncService.setActivationMode(NBSyncService.ActivationMode.OLDER, cutoff);
|
||||
} else {
|
||||
NBSyncService.setActivationMode(NBSyncService.ActivationMode.NEWER, cutoff);
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.swapCursor(cursor);
|
||||
adapter.swapCursor(cursor);
|
||||
}
|
||||
updateLoadingIndicator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
adapter.notifyDataSetInvalidated();
|
||||
if (adapter != null) adapter.notifyDataSetInvalidated();
|
||||
}
|
||||
|
||||
public void setDefaultFeedView(DefaultFeedView value) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.newsblur.fragment;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
|
@ -85,26 +86,28 @@ public class LoginProgressFragment extends Fragment {
|
|||
|
||||
@Override
|
||||
protected void onPostExecute(NewsBlurResponse result) {
|
||||
Context c = getActivity();
|
||||
if (c == null) return; // we might have run past the lifecycle of the activity
|
||||
if (!result.isError()) {
|
||||
final Animation a = AnimationUtils.loadAnimation(getActivity(), R.anim.text_down);
|
||||
final Animation a = AnimationUtils.loadAnimation(c, R.anim.text_down);
|
||||
updateStatus.setText(R.string.login_logged_in);
|
||||
loggingInProgress.setVisibility(View.GONE);
|
||||
updateStatus.startAnimation(a);
|
||||
|
||||
loginProfilePicture.setVisibility(View.VISIBLE);
|
||||
loginProfilePicture.setImageBitmap(UIUtils.roundCorners(PrefsUtils.getUserImage(getActivity()), 10f));
|
||||
loginProfilePicture.setImageBitmap(UIUtils.roundCorners(PrefsUtils.getUserImage(c), 10f));
|
||||
feedProgress.setVisibility(View.VISIBLE);
|
||||
|
||||
final Animation b = AnimationUtils.loadAnimation(getActivity(), R.anim.text_up);
|
||||
final Animation b = AnimationUtils.loadAnimation(c, R.anim.text_up);
|
||||
retrievingFeeds.setText(R.string.login_retrieving_feeds);
|
||||
retrievingFeeds.startAnimation(b);
|
||||
|
||||
Intent startMain = new Intent(getActivity(), Main.class);
|
||||
getActivity().startActivity(startMain);
|
||||
c.startActivity(startMain);
|
||||
|
||||
} else {
|
||||
UIUtils.safeToast(getActivity(), result.getErrorMessage(), Toast.LENGTH_LONG);
|
||||
startActivity(new Intent(getActivity(), Login.class));
|
||||
UIUtils.safeToast(c, result.getErrorMessage(), Toast.LENGTH_LONG);
|
||||
startActivity(new Intent(c, Login.class));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,32 +5,10 @@ import android.app.Fragment;
|
|||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.newsblur.database.BlurDatabaseHelper;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
|
||||
public class NbFragment extends Fragment {
|
||||
|
||||
protected BlurDatabaseHelper dbHelper;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
dbHelper = new BlurDatabaseHelper(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (dbHelper != null) {
|
||||
try {
|
||||
dbHelper.close();
|
||||
} catch (Exception e) {
|
||||
; // Fragment is already dead
|
||||
}
|
||||
}
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pokes the sync service to perform any pending sync actions.
|
||||
*/
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.newsblur.fragment;
|
|||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
|
@ -70,7 +69,6 @@ public class ReadingItemFragment extends NbFragment implements ClassifierDialogF
|
|||
private ImageLoader imageLoader;
|
||||
private String feedColor, feedTitle, feedFade, feedBorder, feedIconUrl, faviconText;
|
||||
private Classifier classifier;
|
||||
private ContentResolver resolver;
|
||||
private NewsblurWebview web;
|
||||
private BroadcastReceiver receiver;
|
||||
private TextView itemAuthors;
|
||||
|
@ -126,7 +124,6 @@ public class ReadingItemFragment extends NbFragment implements ClassifierDialogF
|
|||
apiManager = new APIManager(getActivity());
|
||||
story = getArguments() != null ? (Story) getArguments().getSerializable("story") : null;
|
||||
|
||||
resolver = getActivity().getContentResolver();
|
||||
inflater = getActivity().getLayoutInflater();
|
||||
|
||||
displayFeedDetails = getArguments().getBoolean("displayFeedDetails");
|
||||
|
@ -240,7 +237,7 @@ public class ReadingItemFragment extends NbFragment implements ClassifierDialogF
|
|||
if (altText != null) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setTitle(finalURL);
|
||||
builder.setMessage(altText);
|
||||
builder.setMessage(Html.fromHtml(altText).toString());
|
||||
builder.setPositiveButton(R.string.alert_dialog_openimage, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
|
@ -303,7 +300,7 @@ public class ReadingItemFragment extends NbFragment implements ClassifierDialogF
|
|||
}
|
||||
|
||||
private void setupItemCommentsAndShares(final View view) {
|
||||
new SetupCommentSectionTask(getActivity(), view, getFragmentManager(), inflater, resolver, apiManager, story, imageLoader).execute();
|
||||
new SetupCommentSectionTask(getActivity(), view, getFragmentManager(), inflater, apiManager, story, imageLoader).execute();
|
||||
}
|
||||
|
||||
private void setupItemMetadata() {
|
||||
|
@ -381,8 +378,14 @@ public class ReadingItemFragment extends NbFragment implements ClassifierDialogF
|
|||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse(story.permalink));
|
||||
startActivity(i);
|
||||
try {
|
||||
i.setData(Uri.parse(story.permalink));
|
||||
startActivity(i);
|
||||
} catch (Throwable t) {
|
||||
// we don't actually know if the user will successfully be able to open whatever string
|
||||
// was in the permalink or if the Intent could throw errors
|
||||
Log.e(this.getClass().getName(), "Error opening story by permalink URL.", t);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -596,57 +599,63 @@ public class ReadingItemFragment extends NbFragment implements ClassifierDialogF
|
|||
|
||||
@Override
|
||||
public void sharedCallback(String sharedText, boolean hasAlreadyBeenShared) {
|
||||
view.findViewById(R.id.reading_share_bar).setVisibility(View.VISIBLE);
|
||||
view.findViewById(R.id.share_bar_underline).setVisibility(View.VISIBLE);
|
||||
|
||||
if (!hasAlreadyBeenShared) {
|
||||
|
||||
if (!TextUtils.isEmpty(sharedText)) {
|
||||
View commentView = inflater.inflate(R.layout.include_comment, null);
|
||||
commentView.setTag(SetupCommentSectionTask.COMMENT_VIEW_BY + user.id);
|
||||
try {
|
||||
view.findViewById(R.id.reading_share_bar).setVisibility(View.VISIBLE);
|
||||
view.findViewById(R.id.share_bar_underline).setVisibility(View.VISIBLE);
|
||||
|
||||
if (!hasAlreadyBeenShared) {
|
||||
|
||||
if (!TextUtils.isEmpty(sharedText)) {
|
||||
View commentView = inflater.inflate(R.layout.include_comment, null);
|
||||
commentView.setTag(SetupCommentSectionTask.COMMENT_VIEW_BY + user.id);
|
||||
|
||||
TextView commentText = (TextView) commentView.findViewById(R.id.comment_text);
|
||||
commentText.setTag("commentBy" + user.id);
|
||||
TextView commentText = (TextView) commentView.findViewById(R.id.comment_text);
|
||||
commentText.setTag("commentBy" + user.id);
|
||||
commentText.setText(sharedText);
|
||||
|
||||
TextView commentLocation = (TextView) commentView.findViewById(R.id.comment_location);
|
||||
if (!TextUtils.isEmpty(user.location)) {
|
||||
commentLocation.setText(user.location.toUpperCase());
|
||||
} else {
|
||||
commentLocation.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (PrefsUtils.getUserImage(getActivity()) != null) {
|
||||
ImageView commentImage = (ImageView) commentView.findViewById(R.id.comment_user_image);
|
||||
commentImage.setImageBitmap(UIUtils.roundCorners(PrefsUtils.getUserImage(getActivity()), 10f));
|
||||
}
|
||||
|
||||
TextView commentSharedDate = (TextView) commentView.findViewById(R.id.comment_shareddate);
|
||||
commentSharedDate.setText(R.string.now);
|
||||
|
||||
TextView commentUsername = (TextView) commentView.findViewById(R.id.comment_username);
|
||||
commentUsername.setText(user.username);
|
||||
|
||||
((LinearLayout) view.findViewById(R.id.reading_friend_comment_container)).addView(commentView);
|
||||
|
||||
ViewUtils.setupCommentCount(getActivity(), view, story.commentCount + 1);
|
||||
|
||||
final ImageView image = ViewUtils.createSharebarImage(getActivity(), imageLoader, user.photoUrl, user.id);
|
||||
((FlowLayout) view.findViewById(R.id.reading_social_commentimages)).addView(image);
|
||||
|
||||
} else {
|
||||
ViewUtils.setupShareCount(getActivity(), view, story.sharedUserIds.length + 1);
|
||||
final ImageView image = ViewUtils.createSharebarImage(getActivity(), imageLoader, user.photoUrl, user.id);
|
||||
((FlowLayout) view.findViewById(R.id.reading_social_shareimages)).addView(image);
|
||||
}
|
||||
} else {
|
||||
View commentViewForUser = view.findViewWithTag(SetupCommentSectionTask.COMMENT_VIEW_BY + user.id);
|
||||
TextView commentText = (TextView) view.findViewWithTag(SetupCommentSectionTask.COMMENT_BY + user.id);
|
||||
commentText.setText(sharedText);
|
||||
|
||||
TextView commentLocation = (TextView) commentView.findViewById(R.id.comment_location);
|
||||
if (!TextUtils.isEmpty(user.location)) {
|
||||
commentLocation.setText(user.location.toUpperCase());
|
||||
} else {
|
||||
commentLocation.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (PrefsUtils.getUserImage(getActivity()) != null) {
|
||||
ImageView commentImage = (ImageView) commentView.findViewById(R.id.comment_user_image);
|
||||
commentImage.setImageBitmap(UIUtils.roundCorners(PrefsUtils.getUserImage(getActivity()), 10f));
|
||||
}
|
||||
|
||||
TextView commentSharedDate = (TextView) commentView.findViewById(R.id.comment_shareddate);
|
||||
commentSharedDate.setText(R.string.now);
|
||||
|
||||
TextView commentUsername = (TextView) commentView.findViewById(R.id.comment_username);
|
||||
commentUsername.setText(user.username);
|
||||
|
||||
((LinearLayout) view.findViewById(R.id.reading_friend_comment_container)).addView(commentView);
|
||||
|
||||
ViewUtils.setupCommentCount(getActivity(), view, story.commentCount + 1);
|
||||
|
||||
final ImageView image = ViewUtils.createSharebarImage(getActivity(), imageLoader, user.photoUrl, user.id);
|
||||
((FlowLayout) view.findViewById(R.id.reading_social_commentimages)).addView(image);
|
||||
|
||||
} else {
|
||||
ViewUtils.setupShareCount(getActivity(), view, story.sharedUserIds.length + 1);
|
||||
final ImageView image = ViewUtils.createSharebarImage(getActivity(), imageLoader, user.photoUrl, user.id);
|
||||
((FlowLayout) view.findViewById(R.id.reading_social_shareimages)).addView(image);
|
||||
}
|
||||
} else {
|
||||
View commentViewForUser = view.findViewWithTag(SetupCommentSectionTask.COMMENT_VIEW_BY + user.id);
|
||||
TextView commentText = (TextView) view.findViewWithTag(SetupCommentSectionTask.COMMENT_BY + user.id);
|
||||
commentText.setText(sharedText);
|
||||
|
||||
TextView commentDateText = (TextView) view.findViewWithTag(SetupCommentSectionTask.COMMENT_DATE_BY + user.id);
|
||||
commentDateText.setText(R.string.now);
|
||||
}
|
||||
TextView commentDateText = (TextView) view.findViewWithTag(SetupCommentSectionTask.COMMENT_DATE_BY + user.id);
|
||||
commentDateText.setText(R.string.now);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// this entire method does not respect context state and can be triggered on stale fragments. it should
|
||||
// be replaced with a proper Loader, or it will always risk crashing the application
|
||||
Log.w(this.getClass().getName(), "async error in callback", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -5,9 +5,7 @@ import java.io.Serializable;
|
|||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.DialogInterface;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.app.DialogFragment;
|
||||
|
@ -19,11 +17,11 @@ import android.widget.EditText;
|
|||
import android.widget.Toast;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.database.FeedProvider;
|
||||
import com.newsblur.domain.Comment;
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.domain.UserDetails;
|
||||
import com.newsblur.network.APIManager;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
||||
|
@ -37,9 +35,7 @@ public class ShareDialogFragment extends DialogFragment {
|
|||
private SharedCallbackDialog callback;
|
||||
private Story story;
|
||||
private UserDetails user;
|
||||
private ContentResolver resolver;
|
||||
private boolean hasBeenShared = false;
|
||||
private Cursor commentCursor;
|
||||
private Comment previousComment;
|
||||
private String previouslySavedShareText;
|
||||
private boolean hasShared = false;
|
||||
|
@ -68,7 +64,6 @@ public class ShareDialogFragment extends DialogFragment {
|
|||
sourceUserId = getArguments().getString(SOURCE_USER_ID);
|
||||
|
||||
apiManager = new APIManager(getActivity());
|
||||
resolver = getActivity().getContentResolver();
|
||||
|
||||
for (String sharedUserId : story.sharedUserIds) {
|
||||
if (TextUtils.equals(user.id, sharedUserId)) {
|
||||
|
@ -78,9 +73,7 @@ public class ShareDialogFragment extends DialogFragment {
|
|||
}
|
||||
|
||||
if (hasBeenShared) {
|
||||
commentCursor = resolver.query(FeedProvider.COMMENTS_URI, null, null, new String[] { story.id, user.id }, null);
|
||||
commentCursor.moveToFirst();
|
||||
previousComment = Comment.fromCursor(commentCursor);
|
||||
previousComment = FeedUtils.dbHelper.getComment(story.id, user.id);
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
|
@ -95,7 +88,9 @@ public class ShareDialogFragment extends DialogFragment {
|
|||
int positiveButtonText = R.string.share_this_story;
|
||||
if (hasBeenShared) {
|
||||
positiveButtonText = R.string.edit;
|
||||
commentEditText.setText(previousComment.commentText);
|
||||
if (previousComment != null ) {
|
||||
commentEditText.setText(previousComment.commentText);
|
||||
}
|
||||
} else if (!TextUtils.isEmpty(previouslySavedShareText)) {
|
||||
commentEditText.setText(previouslySavedShareText);
|
||||
}
|
||||
|
@ -144,10 +139,6 @@ public class ShareDialogFragment extends DialogFragment {
|
|||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (commentCursor != null && !commentCursor.isClosed()) {
|
||||
commentCursor.close();
|
||||
}
|
||||
|
||||
if (!hasShared && commentEditText.length() > 0) {
|
||||
previouslySavedShareText = commentEditText.getText().toString();
|
||||
callback.setPreviouslySavedShareText(previouslySavedShareText);
|
||||
|
|
|
@ -75,6 +75,7 @@ public class APIConstants {
|
|||
public static final String PARAMETER_PAGE_NUMBER = "page";
|
||||
public static final String PARAMETER_ORDER = "order";
|
||||
public static final String PARAMETER_READ_FILTER = "read_filter";
|
||||
public static final String PARAMETER_INCLUDE_TIMESTAMPS = "include_timestamps";
|
||||
|
||||
public static final String VALUE_PREFIX_SOCIAL = "social:";
|
||||
public static final String VALUE_ALLSOCIAL = "river:blurblogs"; // the magic value passed to the mark-read API for all social feeds
|
||||
|
|
|
@ -247,7 +247,9 @@ public class APIManager {
|
|||
}
|
||||
|
||||
public UnreadStoryHashesResponse getUnreadStoryHashes() {
|
||||
APIResponse response = get(APIConstants.URL_UNREAD_HASHES);
|
||||
ValueMultimap values = new ValueMultimap();
|
||||
values.put(APIConstants.PARAMETER_INCLUDE_TIMESTAMPS, "1");
|
||||
APIResponse response = get(APIConstants.URL_UNREAD_HASHES, values);
|
||||
return (UnreadStoryHashesResponse) response.getResponse(gson, UnreadStoryHashesResponse.class);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,11 +3,12 @@ package com.newsblur.network;
|
|||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.app.DialogFragment;
|
||||
import android.app.FragmentManager;
|
||||
|
@ -23,14 +24,13 @@ import android.widget.TextView;
|
|||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.activity.Profile;
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.database.FeedProvider;
|
||||
import com.newsblur.domain.Comment;
|
||||
import com.newsblur.domain.Reply;
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.domain.UserDetails;
|
||||
import com.newsblur.domain.UserProfile;
|
||||
import com.newsblur.fragment.ReplyDialogFragment;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.ImageLoader;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ViewUtils;
|
||||
|
@ -43,7 +43,6 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
|
|||
|
||||
private ArrayList<View> publicCommentViews;
|
||||
private ArrayList<View> friendCommentViews;
|
||||
private final ContentResolver resolver;
|
||||
private final APIManager apiManager;
|
||||
|
||||
private final Story story;
|
||||
|
@ -53,13 +52,12 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
|
|||
private final Context context;
|
||||
private UserDetails user;
|
||||
private final FragmentManager manager;
|
||||
private Cursor commentCursor;
|
||||
private List<Comment> comments;
|
||||
|
||||
public SetupCommentSectionTask(final Context context, final View view, final FragmentManager manager, LayoutInflater inflater, final ContentResolver resolver, final APIManager apiManager, final Story story, final ImageLoader imageLoader) {
|
||||
public SetupCommentSectionTask(final Context context, final View view, final FragmentManager manager, LayoutInflater inflater, final APIManager apiManager, final Story story, final ImageLoader imageLoader) {
|
||||
this.context = context;
|
||||
this.manager = manager;
|
||||
this.inflater = inflater;
|
||||
this.resolver = resolver;
|
||||
this.apiManager = apiManager;
|
||||
this.story = story;
|
||||
this.imageLoader = imageLoader;
|
||||
|
@ -69,23 +67,18 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
|
|||
|
||||
@Override
|
||||
protected Void doInBackground(Void... arg0) {
|
||||
|
||||
commentCursor = resolver.query(FeedProvider.COMMENTS_URI, null, null, new String[] { story.id }, null);
|
||||
comments = FeedUtils.dbHelper.getComments(story.id);
|
||||
|
||||
publicCommentViews = new ArrayList<View>();
|
||||
friendCommentViews = new ArrayList<View>();
|
||||
|
||||
while (commentCursor.moveToNext()) {
|
||||
final Comment comment = Comment.fromCursor(commentCursor);
|
||||
|
||||
for (final Comment comment : comments) {
|
||||
// skip public comments if they are disabled
|
||||
if (!comment.byFriend && !PrefsUtils.showPublicComments(context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Cursor userCursor = resolver.query(FeedProvider.USERS_URI, null, DatabaseConstants.USER_USERID + " IN (?)", new String[] { comment.userId }, null);
|
||||
UserProfile commentUser = UserProfile.fromCursor(userCursor);
|
||||
userCursor.close();
|
||||
UserProfile commentUser = FeedUtils.dbHelper.getUserProfile(comment.userId);
|
||||
// rarely, we get a comment but never got the user's profile, so we can't display it
|
||||
if (commentUser == null) {
|
||||
Log.w(this.getClass().getName(), "cannot display comment from missing user ID: " + comment.userId);
|
||||
|
@ -118,9 +111,7 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
|
|||
for (String id : comment.likingUsers) {
|
||||
ImageView favouriteImage = new ImageView(context);
|
||||
|
||||
Cursor likingUserCursor = resolver.query(FeedProvider.USERS_URI, null, DatabaseConstants.USER_USERID + " IN (?)", new String[] { id }, null);
|
||||
UserProfile user = UserProfile.fromCursor(likingUserCursor);
|
||||
likingUserCursor.close();
|
||||
UserProfile user = FeedUtils.dbHelper.getUserProfile(id);
|
||||
|
||||
imageLoader.displayImage(user.photoUrl, favouriteImage, 10f);
|
||||
favouriteImage.setTag(id);
|
||||
|
@ -144,9 +135,7 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
|
|||
@Override
|
||||
public void onClick(View v) {
|
||||
if (story != null) {
|
||||
Cursor userCursor = resolver.query(FeedProvider.USERS_URI, null, DatabaseConstants.USER_USERID + " IN (?)", new String[] { comment.userId }, null);
|
||||
UserProfile user = UserProfile.fromCursor(userCursor);
|
||||
userCursor.close();
|
||||
UserProfile user = FeedUtils.dbHelper.getUserProfile(comment.userId);
|
||||
|
||||
DialogFragment newFragment = ReplyDialogFragment.newInstance(story, comment.userId, user.username);
|
||||
newFragment.show(manager, "dialog");
|
||||
|
@ -154,19 +143,15 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
|
|||
}
|
||||
});
|
||||
|
||||
Cursor replies = resolver.query(FeedProvider.REPLIES_URI, null, null, new String[] { comment.id }, DatabaseConstants.REPLY_DATE + " DESC");
|
||||
while (replies.moveToNext()) {
|
||||
Reply reply = Reply.fromCursor(replies);
|
||||
|
||||
List<Reply> replies = FeedUtils.dbHelper.getCommentReplies(comment.id);
|
||||
for (Reply reply : replies) {
|
||||
View replyView = inflater.inflate(R.layout.include_reply, null);
|
||||
TextView replyText = (TextView) replyView.findViewById(R.id.reply_text);
|
||||
replyText.setText(Html.fromHtml(reply.text));
|
||||
ImageView replyImage = (ImageView) replyView.findViewById(R.id.reply_user_image);
|
||||
|
||||
// occasionally there was no reply user and this caused a force close
|
||||
Cursor replyCursor = resolver.query(FeedProvider.USERS_URI, null, DatabaseConstants.USER_USERID + " IN (?)", new String[] { reply.userId }, null);
|
||||
if (replyCursor.getCount() > 0) {
|
||||
final UserProfile replyUser = UserProfile.fromCursor(replyCursor);
|
||||
final UserProfile replyUser = FeedUtils.dbHelper.getUserProfile(reply.userId);
|
||||
if (replyUser != null) {
|
||||
imageLoader.displayImage(replyUser.photoUrl, replyImage);
|
||||
replyImage.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
|
@ -183,14 +168,12 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
|
|||
TextView replyUsername = (TextView) replyView.findViewById(R.id.reply_username);
|
||||
replyUsername.setText(R.string.unknown_user);
|
||||
}
|
||||
replyCursor.close();
|
||||
|
||||
TextView replySharedDate = (TextView) replyView.findViewById(R.id.reply_shareddate);
|
||||
replySharedDate.setText(reply.shortDate + " ago");
|
||||
|
||||
((LinearLayout) commentView.findViewById(R.id.comment_replies_container)).addView(replyView);
|
||||
}
|
||||
replies.close();
|
||||
|
||||
TextView commentUsername = (TextView) commentView.findViewById(R.id.comment_username);
|
||||
commentUsername.setText(commentUser.username);
|
||||
|
@ -212,15 +195,11 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
|
|||
commentImage.setVisibility(View.INVISIBLE);
|
||||
|
||||
|
||||
Cursor sourceUserCursor = resolver.query(FeedProvider.USERS_URI, null, DatabaseConstants.USER_USERID + " IN (?)", new String[] { comment.sourceUserId }, null);
|
||||
if (sourceUserCursor.getCount() > 0) {
|
||||
UserProfile sourceUser = UserProfile.fromCursor(sourceUserCursor);
|
||||
sourceUserCursor.close();
|
||||
|
||||
UserProfile sourceUser = FeedUtils.dbHelper.getUserProfile(comment.sourceUserId);
|
||||
if (sourceUser != null) {
|
||||
imageLoader.displayImage(sourceUser.photoUrl, sourceUserImage, 10f);
|
||||
imageLoader.displayImage(userPhoto, usershareImage, 10f);
|
||||
}
|
||||
sourceUserCursor.close();
|
||||
} else {
|
||||
imageLoader.displayImage(userPhoto, commentImage, 10f);
|
||||
}
|
||||
|
@ -252,42 +231,28 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
|
|||
TextView friendCommentTotal = ((TextView) viewHolder.get().findViewById(R.id.reading_friend_comment_total));
|
||||
TextView publicCommentTotal = ((TextView) viewHolder.get().findViewById(R.id.reading_public_comment_total));
|
||||
|
||||
ViewUtils.setupCommentCount(context, viewHolder.get(), commentCursor.getCount());
|
||||
ViewUtils.setupCommentCount(context, viewHolder.get(), comments.size());
|
||||
ViewUtils.setupShareCount(context, viewHolder.get(), story.sharedUserIds.length);
|
||||
|
||||
ArrayList<String> commentIds = new ArrayList<String>();
|
||||
commentCursor.moveToFirst();
|
||||
for (int i = 0; i < commentCursor.getCount(); i++) {
|
||||
commentIds.add(commentCursor.getString(commentCursor.getColumnIndex(DatabaseConstants.COMMENT_USERID)));
|
||||
commentCursor.moveToNext();
|
||||
Set<String> commentIds = new HashSet<String>();
|
||||
for (Comment comment : comments) {
|
||||
commentIds.add(comment.userId);
|
||||
}
|
||||
|
||||
for (final String userId : story.sharedUserIds) {
|
||||
for (String userId : story.sharedUserIds) {
|
||||
if (!commentIds.contains(userId)) {
|
||||
Cursor userCursor = resolver.query(FeedProvider.USERS_URI, null, DatabaseConstants.USER_USERID + " IN (?)", new String[] { userId }, null);
|
||||
if (userCursor.getCount() > 0) {
|
||||
UserProfile user = UserProfile.fromCursor(userCursor);
|
||||
userCursor.close();
|
||||
|
||||
UserProfile user = FeedUtils.dbHelper.getUserProfile(userId);
|
||||
if (user != null) {
|
||||
ImageView image = ViewUtils.createSharebarImage(context, imageLoader, user.photoUrl, user.userId);
|
||||
sharedGrid.addView(image);
|
||||
}
|
||||
userCursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
commentCursor.moveToFirst();
|
||||
|
||||
for (int i = 0; i < commentCursor.getCount(); i++) {
|
||||
final Comment comment = Comment.fromCursor(commentCursor);
|
||||
|
||||
Cursor userCursor = resolver.query(FeedProvider.USERS_URI, null, DatabaseConstants.USER_USERID + " IN (?)", new String[] { comment.userId }, null);
|
||||
UserProfile user = UserProfile.fromCursor(userCursor);
|
||||
userCursor.close();
|
||||
|
||||
for (Comment comment : comments) {
|
||||
UserProfile user = FeedUtils.dbHelper.getUserProfile(comment.userId);
|
||||
ImageView image = ViewUtils.createSharebarImage(context, imageLoader, user.photoUrl, user.userId);
|
||||
commentGrid.addView(image);
|
||||
commentCursor.moveToNext();
|
||||
}
|
||||
|
||||
if (publicCommentViews.size() > 0) {
|
||||
|
@ -323,7 +288,6 @@ public class SetupCommentSectionTask extends AsyncTask<Void, Void, Void> {
|
|||
}
|
||||
|
||||
}
|
||||
commentCursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,11 +2,13 @@ package com.newsblur.network.domain;
|
|||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class UnreadStoryHashesResponse extends NewsBlurResponse {
|
||||
|
||||
@SerializedName("unread_feed_story_hashes")
|
||||
public Map<String,String[]> unreadHashes;
|
||||
public Map<String,List<String[]>> unreadHashes;
|
||||
// the inner, key-less array contains an ordered pair of story hash and timestamp
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
package com.newsblur.service;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.network.domain.StoriesResponse;
|
||||
import com.newsblur.network.domain.UnreadStoryHashesResponse;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.ImageCache;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
public class ImagePrefetchService extends SubService {
|
||||
|
||||
private static volatile boolean Running = false;
|
||||
|
||||
ImageCache imageCache;
|
||||
|
||||
/** URLs of images contained in recently fetched stories that are candidates for prefetch. */
|
||||
static Set<String> ImageQueue;
|
||||
static { ImageQueue = new HashSet<String>(); }
|
||||
|
||||
public ImagePrefetchService(NBSyncService parent) {
|
||||
super(parent);
|
||||
imageCache = new ImageCache(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void exec() {
|
||||
if (!PrefsUtils.isImagePrefetchEnabled(parent)) return;
|
||||
if (ImageQueue.size() < 1) return;
|
||||
|
||||
gotWork();
|
||||
|
||||
while ((ImageQueue.size() > 0) && PrefsUtils.isImagePrefetchEnabled(parent)) {
|
||||
startExpensiveCycle();
|
||||
Set<String> fetchedImages = new HashSet<String>();
|
||||
Set<String> batch = new HashSet<String>(AppConstants.IMAGE_PREFETCH_BATCH_SIZE);
|
||||
batchloop: for (String url : ImageQueue) {
|
||||
batch.add(url);
|
||||
if (batch.size() >= AppConstants.IMAGE_PREFETCH_BATCH_SIZE) break batchloop;
|
||||
}
|
||||
try {
|
||||
for (String url : batch) {
|
||||
if (parent.stopSync()) return;
|
||||
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "prefetching image: " + url);
|
||||
imageCache.cacheImage(url);
|
||||
|
||||
fetchedImages.add(url);
|
||||
}
|
||||
} finally {
|
||||
ImageQueue.removeAll(fetchedImages);
|
||||
gotWork();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void addUrl(String url) {
|
||||
ImageQueue.add(url);
|
||||
}
|
||||
|
||||
public static int getPendingCount() {
|
||||
return ImageQueue.size();
|
||||
}
|
||||
|
||||
public static boolean running() {
|
||||
return Running;
|
||||
}
|
||||
@Override
|
||||
protected void setRunning(boolean running) {
|
||||
Running = running;
|
||||
}
|
||||
@Override
|
||||
protected boolean isRunning() {
|
||||
return Running;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.newsblur.service;
|
|||
import android.app.Service;
|
||||
import android.content.ComponentCallbacks2;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.IBinder;
|
||||
|
@ -23,13 +24,10 @@ import com.newsblur.network.APIManager;
|
|||
import com.newsblur.network.domain.FeedFolderResponse;
|
||||
import com.newsblur.network.domain.NewsBlurResponse;
|
||||
import com.newsblur.network.domain.StoriesResponse;
|
||||
import com.newsblur.network.domain.StoryTextResponse;
|
||||
import com.newsblur.network.domain.UnreadStoryHashesResponse;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.FeedSet;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.ImageCache;
|
||||
import com.newsblur.util.FileCache;
|
||||
import com.newsblur.util.NetworkUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.ReadingAction;
|
||||
|
@ -64,30 +62,38 @@ import java.util.concurrent.TimeUnit;
|
|||
*/
|
||||
public class NBSyncService extends Service {
|
||||
|
||||
/**
|
||||
* Mode switch for which newly received stories are suitable for display so
|
||||
* that they don't disrupt actively visible pager and list offsets.
|
||||
*/
|
||||
public enum ActivationMode { ALL, OLDER, NEWER };
|
||||
|
||||
private static final Object WAKELOCK_MUTEX = new Object();
|
||||
private static final Object PENDING_FEED_MUTEX = new Object();
|
||||
|
||||
private volatile static boolean ActionsRunning = false;
|
||||
private volatile static boolean CleanupRunning = false;
|
||||
private volatile static boolean FFSyncRunning = false;
|
||||
private volatile static boolean UnreadSyncRunning = false;
|
||||
private volatile static boolean UnreadHashSyncRunning = false;
|
||||
private volatile static boolean StorySyncRunning = false;
|
||||
private volatile static boolean OriginalTextSyncRunning = false;
|
||||
private volatile static boolean ImagePrefetchRunning = false;
|
||||
private volatile static boolean HousekeepingRunning = false;
|
||||
|
||||
/** Don't do any actions that might modify the story list for a feed or folder in a way that
|
||||
would annoy a user who is on the story list or paging through stories. */
|
||||
private volatile static boolean HoldStories = false;
|
||||
private volatile static boolean DoFeedsFolders = false;
|
||||
private volatile static boolean isMemoryLow = false;
|
||||
private volatile static boolean DoUnreads = false;
|
||||
private volatile static boolean HaltNow = false;
|
||||
private volatile static ActivationMode ActMode = ActivationMode.ALL;
|
||||
private volatile static long ModeCutoff = 0L;
|
||||
|
||||
public volatile static Boolean isPremium = null;
|
||||
public volatile static Boolean isStaff = null;
|
||||
|
||||
private volatile static boolean isMemoryLow = false;
|
||||
private static long lastFeedCount = 0L;
|
||||
private static long lastFFWriteMillis = 0L;
|
||||
|
||||
/** Feed sets that we need to sync and how many stories the UI wants for them. */
|
||||
private static Map<FeedSet,Integer> PendingFeeds;
|
||||
static { PendingFeeds = new HashMap<FeedSet,Integer>(); }
|
||||
/** Feed set that we need to sync immediately for the UI. */
|
||||
private static FeedSet PendingFeed;
|
||||
private static Integer PendingFeedTarget = 0;
|
||||
|
||||
/** Feed sets that the API has said to have no more pages left. */
|
||||
private static Set<FeedSet> ExhaustedFeeds;
|
||||
static { ExhaustedFeeds = new HashSet<FeedSet>(); }
|
||||
|
@ -98,27 +104,20 @@ public class NBSyncService extends Service {
|
|||
private static Map<FeedSet,Integer> FeedStoriesSeen;
|
||||
static { FeedStoriesSeen = new HashMap<FeedSet,Integer>(); }
|
||||
|
||||
/** Unread story hashes the API listed that we do not appear to have locally yet. */
|
||||
private static Set<String> StoryHashQueue;
|
||||
static { StoryHashQueue = new HashSet<String>(); }
|
||||
|
||||
/** URLs of images contained in recently fetched stories that are candidates for prefetch. */
|
||||
private static Set<String> ImageQueue;
|
||||
static { ImageQueue = new HashSet<String>(); }
|
||||
|
||||
/** Stories for which we want to fetch original text data. */
|
||||
private static Set<String> OriginalTextQueue;
|
||||
static { OriginalTextQueue = new HashSet<String>(); }
|
||||
|
||||
/** Actions that may need to be double-checked locally due to overlapping API calls. */
|
||||
private static List<ReadingAction> FollowupActions;
|
||||
static { FollowupActions = new ArrayList<ReadingAction>(); }
|
||||
|
||||
private PowerManager.WakeLock wl = null;
|
||||
private ExecutorService executor;
|
||||
private APIManager apiManager;
|
||||
private BlurDatabaseHelper dbHelper;
|
||||
private ImageCache imageCache;
|
||||
Set<String> orphanFeedIds;
|
||||
|
||||
private ExecutorService primaryExecutor;
|
||||
OriginalTextService originalTextService;
|
||||
UnreadsService unreadsService;
|
||||
ImagePrefetchService imagePrefetchService;
|
||||
|
||||
PowerManager.WakeLock wl = null;
|
||||
APIManager apiManager;
|
||||
BlurDatabaseHelper dbHelper;
|
||||
private int lastStartIdCompleted = -1;
|
||||
|
||||
@Override
|
||||
|
@ -128,14 +127,25 @@ public class NBSyncService extends Service {
|
|||
HaltNow = false;
|
||||
PowerManager pm = (PowerManager) getApplicationContext().getSystemService(POWER_SERVICE);
|
||||
wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getSimpleName());
|
||||
wl.setReferenceCounted(false);
|
||||
executor = Executors.newFixedThreadPool(1);
|
||||
apiManager = new APIManager(this);
|
||||
PrefsUtils.checkForUpgrade(this);
|
||||
dbHelper = new BlurDatabaseHelper(this);
|
||||
imageCache = new ImageCache(this);
|
||||
wl.setReferenceCounted(true);
|
||||
|
||||
primaryExecutor = Executors.newFixedThreadPool(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Services can be constructed synchrnously by the Main thread, so don't do expensive
|
||||
* parts of construction in onCreate, but save them for when we are in our own thread.
|
||||
*/
|
||||
private void finishConstruction() {
|
||||
if (apiManager == null) {
|
||||
apiManager = new APIManager(this);
|
||||
dbHelper = new BlurDatabaseHelper(this);
|
||||
originalTextService = new OriginalTextService(this);
|
||||
unreadsService = new UnreadsService(this);
|
||||
imagePrefetchService = new ImagePrefetchService(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called serially, once per "start" of the service. This serves as a wakeup call
|
||||
* that the service should check for outstanding work.
|
||||
|
@ -148,11 +158,10 @@ public class NBSyncService extends Service {
|
|||
// allowed to do tangible work. We spawn a thread to do so.
|
||||
Runnable r = new Runnable() {
|
||||
public void run() {
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
||||
doSync(startId);
|
||||
}
|
||||
};
|
||||
executor.execute(r);
|
||||
primaryExecutor.execute(r);
|
||||
} else {
|
||||
Log.d(this.getClass().getName(), "Skipping sync: app not active and background sync not enabled.");
|
||||
stopSelf(startId);
|
||||
|
@ -166,16 +175,26 @@ public class NBSyncService extends Service {
|
|||
/**
|
||||
* Do the actual work of syncing.
|
||||
*/
|
||||
private synchronized void doSync(int startId) {
|
||||
private synchronized void doSync(final int startId) {
|
||||
try {
|
||||
if (HaltNow) {
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "skipping sync, soft interrupt set.");
|
||||
return;
|
||||
if (HaltNow) return;
|
||||
|
||||
incrementRunningChild();
|
||||
finishConstruction();
|
||||
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "starting primary sync");
|
||||
|
||||
if (NbActivity.getActiveActivityCount() < 1) {
|
||||
// if the UI isn't running, politely run at background priority
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
||||
} else {
|
||||
// if the UI is running, run just one step below normal priority so we don't step on async tasks that are updating the UI
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT + Process.THREAD_PRIORITY_LESS_FAVORABLE);
|
||||
}
|
||||
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "starting sync . . .");
|
||||
|
||||
wl.acquire();
|
||||
// do this even if background syncs aren't enabled, because it absolutely must happen
|
||||
// on all devices
|
||||
housekeeping();
|
||||
|
||||
// check to see if we are on an allowable network only after ensuring we have CPU
|
||||
if (!(PrefsUtils.isBackgroundNetworkAllowed(this) || (NbActivity.getActiveActivityCount() > 0))) {
|
||||
|
@ -183,31 +202,67 @@ public class NBSyncService extends Service {
|
|||
return;
|
||||
}
|
||||
|
||||
originalTextService.start(startId);
|
||||
|
||||
// first: catch up
|
||||
syncActions();
|
||||
|
||||
// these requests are expressly enqueued by the UI/user, do them next
|
||||
syncPendingFeeds();
|
||||
syncPendingFeedStories();
|
||||
|
||||
syncMetadata();
|
||||
syncMetadata(startId);
|
||||
|
||||
syncUnreads();
|
||||
unreadsService.start(startId);
|
||||
|
||||
imagePrefetchService.start(startId);
|
||||
|
||||
finishActions();
|
||||
|
||||
syncOriginalTexts();
|
||||
|
||||
prefetchImages();
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "finishing primary sync");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(this.getClass().getName(), "Sync error.", e);
|
||||
} finally {
|
||||
if (NbActivity.getActiveActivityCount() < 1) {
|
||||
stopSelf(startId);
|
||||
decrementRunningChild(startId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for upgrades and wipe the DB if necessary, and do DB maintenance
|
||||
*/
|
||||
private void housekeeping() {
|
||||
try {
|
||||
boolean upgraded = PrefsUtils.checkForUpgrade(this);
|
||||
if (upgraded) {
|
||||
HousekeepingRunning = true;
|
||||
NbActivity.updateAllActivities(false);
|
||||
// wipe the local DB
|
||||
dbHelper.dropAndRecreateTables();
|
||||
NbActivity.updateAllActivities(true);
|
||||
// in case this is the first time we have run since moving the cache to the new location,
|
||||
// blow away the old version entirely. This line can be removed some time well after
|
||||
// v61+ is widely deployed
|
||||
FileCache.cleanUpOldCache(this);
|
||||
PrefsUtils.updateVersion(this);
|
||||
}
|
||||
|
||||
boolean autoVac = PrefsUtils.isTimeToVacuum(this);
|
||||
// this will lock up the DB for a few seconds, only do it if the UI is hidden
|
||||
if (NbActivity.getActiveActivityCount() > 0) autoVac = false;
|
||||
|
||||
if (upgraded || autoVac) {
|
||||
HousekeepingRunning = true;
|
||||
NbActivity.updateAllActivities(false);
|
||||
PrefsUtils.updateLastVacuumTime(this);
|
||||
Log.i(this.getClass().getName(), "rebuilding DB . . .");
|
||||
dbHelper.vacuum();
|
||||
Log.i(this.getClass().getName(), ". . . . done rebuilding DB");
|
||||
}
|
||||
} finally {
|
||||
if (HousekeepingRunning) {
|
||||
HousekeepingRunning = false;
|
||||
NbActivity.updateAllActivities(true);
|
||||
}
|
||||
lastStartIdCompleted = startId;
|
||||
if (wl != null) wl.release();
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), " . . . sync done");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,7 +278,7 @@ public class NBSyncService extends Service {
|
|||
if (c.getCount() < 1) return;
|
||||
|
||||
ActionsRunning = true;
|
||||
NbActivity.updateAllActivities();
|
||||
NbActivity.updateAllActivities(false);
|
||||
|
||||
actionsloop : while (c.moveToNext()) {
|
||||
String id = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_ID));
|
||||
|
@ -252,7 +307,7 @@ public class NBSyncService extends Service {
|
|||
closeQuietly(c);
|
||||
if (ActionsRunning) {
|
||||
ActionsRunning = false;
|
||||
NbActivity.updateAllActivities();
|
||||
NbActivity.updateAllActivities(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -269,15 +324,16 @@ public class NBSyncService extends Service {
|
|||
ra.doLocal(dbHelper);
|
||||
}
|
||||
FollowupActions.clear();
|
||||
NbActivity.updateAllActivities(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* The very first step of a sync - get the feed/folder list, unread counts, and
|
||||
* unread hashes. Doing this resets pagination on the server!
|
||||
*/
|
||||
private void syncMetadata() {
|
||||
private void syncMetadata(int startId) {
|
||||
if (stopSync()) return;
|
||||
if (HoldStories) return;
|
||||
if (ActMode != ActivationMode.ALL) return;
|
||||
|
||||
if (DoFeedsFolders || PrefsUtils.isTimeToAutoSync(this)) {
|
||||
PrefsUtils.updateLastSyncTime(this);
|
||||
|
@ -288,29 +344,30 @@ public class NBSyncService extends Service {
|
|||
|
||||
// cleanup is expensive, so do it as part of the metadata sync
|
||||
CleanupRunning = true;
|
||||
NbActivity.updateAllActivities();
|
||||
NbActivity.updateAllActivities(false);
|
||||
dbHelper.cleanupStories(PrefsUtils.isKeepOldStories(this));
|
||||
imageCache.cleanup();
|
||||
dbHelper.cleanupStoryText();
|
||||
imagePrefetchService.imageCache.cleanup(dbHelper.getAllStoryImages());
|
||||
CleanupRunning = false;
|
||||
NbActivity.updateAllActivities();
|
||||
NbActivity.updateAllActivities(false);
|
||||
|
||||
// cleanup may have taken a while, so re-check our running status
|
||||
if (stopSync()) return;
|
||||
if (HoldStories) return;
|
||||
if (ActMode != ActivationMode.ALL) return;
|
||||
|
||||
FFSyncRunning = true;
|
||||
NbActivity.updateAllActivities();
|
||||
NbActivity.updateAllActivities(false);
|
||||
|
||||
// there is a rare issue with feeds that have no folder. capture them for workarounds.
|
||||
List<String> debugFeedIds = new ArrayList<String>();
|
||||
Set<String> debugFeedIds = new HashSet<String>();
|
||||
orphanFeedIds = new HashSet<String>();
|
||||
|
||||
try {
|
||||
// a metadata sync invalidates pagination and feed status
|
||||
ExhaustedFeeds.clear();
|
||||
FeedPagesSeen.clear();
|
||||
FeedStoriesSeen.clear();
|
||||
StoryHashQueue.clear();
|
||||
UnreadsService.clearHashes();
|
||||
|
||||
FeedFolderResponse feedResponse = apiManager.getFolderFeedMapping(true);
|
||||
|
||||
|
@ -358,14 +415,19 @@ public class NBSyncService extends Service {
|
|||
|
||||
// data for the feeds table
|
||||
List<ContentValues> feedValues = new ArrayList<ContentValues>();
|
||||
for (String feedId : feedResponse.feeds.keySet()) {
|
||||
feedaddloop: for (String feedId : feedResponse.feeds.keySet()) {
|
||||
// sanity-check that the returned feeds actually exist in a folder or at the root
|
||||
// if they do not, they should neither display nor count towards unread numbers
|
||||
if (debugFeedIds.contains(feedId)) {
|
||||
feedValues.add(feedResponse.feeds.get(feedId).getValues());
|
||||
} else {
|
||||
if (! debugFeedIds.contains(feedId)) {
|
||||
Log.w(this.getClass().getName(), "Found and ignoring un-foldered feed: " + feedId );
|
||||
orphanFeedIds.add(feedId);
|
||||
continue feedaddloop;
|
||||
}
|
||||
if (! feedResponse.feeds.get(feedId).active) {
|
||||
// the feed is disabled/hidden, pretend it doesn't exist
|
||||
continue feedaddloop;
|
||||
}
|
||||
feedValues.add(feedResponse.feeds.get(feedId).getValues());
|
||||
}
|
||||
|
||||
// data for the the social feeds table
|
||||
|
@ -382,225 +444,85 @@ public class NBSyncService extends Service {
|
|||
lastFFWriteMillis = System.currentTimeMillis() - startTime;
|
||||
lastFeedCount = feedValues.size();
|
||||
|
||||
unreadsService.start(startId);
|
||||
UnreadsService.doMetadata();
|
||||
|
||||
} finally {
|
||||
FFSyncRunning = false;
|
||||
NbActivity.updateAllActivities();
|
||||
NbActivity.updateAllActivities(true);
|
||||
}
|
||||
|
||||
if (HaltNow) return;
|
||||
if (HoldStories) return;
|
||||
|
||||
UnreadHashSyncRunning = true;
|
||||
|
||||
try {
|
||||
|
||||
// only use the unread status API if the user is premium
|
||||
if (isPremium) {
|
||||
UnreadStoryHashesResponse unreadHashes = apiManager.getUnreadStoryHashes();
|
||||
|
||||
// note all the stories we thought were unread before. if any fail to appear in
|
||||
// the API request for unreads, we will mark them as read
|
||||
List<String> oldUnreadHashes = dbHelper.getUnreadStoryHashes();
|
||||
|
||||
for (Entry<String, String[]> entry : unreadHashes.unreadHashes.entrySet()) {
|
||||
String feedId = entry.getKey();
|
||||
// ignore unreads from orphaned feeds
|
||||
if( debugFeedIds.contains(feedId)) {
|
||||
// only fetch the reported unreads if we don't already have them
|
||||
List<String> existingHashes = dbHelper.getStoryHashesForFeed(feedId);
|
||||
for (String newHash : entry.getValue()) {
|
||||
if (!existingHashes.contains(newHash)) {
|
||||
StoryHashQueue.add(newHash);
|
||||
}
|
||||
oldUnreadHashes.remove(newHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dbHelper.markStoryHashesRead(oldUnreadHashes);
|
||||
} else {
|
||||
// if the user isn't premium, go so far as to clean up everything, there is no offline support
|
||||
dbHelper.cleanupAllStories();
|
||||
}
|
||||
} finally {
|
||||
UnreadHashSyncRunning = false;
|
||||
NbActivity.updateAllActivities();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch any unread stories (by hash) that we learnt about during the FFSync.
|
||||
*/
|
||||
private void syncUnreads() {
|
||||
try {
|
||||
unreadsyncloop: while (StoryHashQueue.size() > 0) {
|
||||
if (stopSync()) return;
|
||||
if (HoldStories) return;
|
||||
|
||||
UnreadSyncRunning = true;
|
||||
NbActivity.updateAllActivities();
|
||||
|
||||
List<String> hashBatch = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE);
|
||||
batchloop: for (String hash : StoryHashQueue) {
|
||||
hashBatch.add(hash);
|
||||
if (hashBatch.size() >= AppConstants.UNREAD_FETCH_BATCH_SIZE) break batchloop;
|
||||
}
|
||||
StoriesResponse response = apiManager.getStoriesByHash(hashBatch);
|
||||
if (! isStoryResponseGood(response)) {
|
||||
Log.e(this.getClass().getName(), "error fetching unreads batch, abandoning sync.");
|
||||
break unreadsyncloop;
|
||||
}
|
||||
dbHelper.insertStories(response);
|
||||
for (String hash : hashBatch) {
|
||||
StoryHashQueue.remove(hash);
|
||||
}
|
||||
|
||||
for (Story story : response.stories) {
|
||||
if (story.imageUrls != null) {
|
||||
for (String url : story.imageUrls) {
|
||||
ImageQueue.add(url);
|
||||
}
|
||||
}
|
||||
DefaultFeedView mode = PrefsUtils.getDefaultFeedViewForFeed(this, story.feedId);
|
||||
if (mode == DefaultFeedView.TEXT) {
|
||||
OriginalTextQueue.add(story.storyHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (UnreadSyncRunning) {
|
||||
UnreadSyncRunning = false;
|
||||
NbActivity.updateAllActivities();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void syncOriginalTexts() {
|
||||
try {
|
||||
while (OriginalTextQueue.size() > 0) {
|
||||
OriginalTextSyncRunning = true;
|
||||
NbActivity.updateAllActivities();
|
||||
|
||||
Set<String> fetchedHashes = new HashSet<String>();
|
||||
Set<String> batch = new HashSet<String>(AppConstants.IMAGE_PREFETCH_BATCH_SIZE);
|
||||
batchloop: for (String hash : OriginalTextQueue) {
|
||||
batch.add(hash);
|
||||
if (batch.size() >= AppConstants.IMAGE_PREFETCH_BATCH_SIZE) break batchloop;
|
||||
}
|
||||
try {
|
||||
fetchloop: for (String hash : batch) {
|
||||
if (stopSync()) return;
|
||||
|
||||
String result = "";
|
||||
StoryTextResponse response = apiManager.getStoryText(FeedUtils.inferFeedId(hash), hash);
|
||||
if ((response != null) && (response.originalText != null)) {
|
||||
result = response.originalText;
|
||||
}
|
||||
dbHelper.putStoryText(hash, result);
|
||||
|
||||
fetchedHashes.add(hash);
|
||||
}
|
||||
} finally {
|
||||
OriginalTextQueue.removeAll(fetchedHashes);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (OriginalTextSyncRunning) {
|
||||
OriginalTextSyncRunning = false;
|
||||
NbActivity.updateAllActivities();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch stories needed because the user is actively viewing a feed or folder.
|
||||
*/
|
||||
private void syncPendingFeeds() {
|
||||
private void syncPendingFeedStories() {
|
||||
FeedSet fs = PendingFeed;
|
||||
boolean finished = false;
|
||||
if (fs == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Set<FeedSet> handledFeeds = new HashSet<FeedSet>();
|
||||
feedloop: for (FeedSet fs : PendingFeeds.keySet()) {
|
||||
if (ExhaustedFeeds.contains(fs)) {
|
||||
Log.i(this.getClass().getName(), "No more stories for feed set: " + fs);
|
||||
finished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!FeedPagesSeen.containsKey(fs)) {
|
||||
FeedPagesSeen.put(fs, 0);
|
||||
FeedStoriesSeen.put(fs, 0);
|
||||
}
|
||||
int pageNumber = FeedPagesSeen.get(fs);
|
||||
int totalStoriesSeen = FeedStoriesSeen.get(fs);
|
||||
|
||||
if (ExhaustedFeeds.contains(fs)) {
|
||||
Log.i(this.getClass().getName(), "No more stories for feed set: " + fs);
|
||||
handledFeeds.add(fs);
|
||||
continue feedloop;
|
||||
StoryOrder order = PrefsUtils.getStoryOrder(this, fs);
|
||||
ReadFilter filter = PrefsUtils.getReadFilter(this, fs);
|
||||
|
||||
while (totalStoriesSeen < PendingFeedTarget) {
|
||||
if (stopSync()) return;
|
||||
|
||||
if (!fs.equals(PendingFeed)) {
|
||||
// the active view has changed
|
||||
if (fs == null) finished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
StorySyncRunning = true;
|
||||
NbActivity.updateAllActivities();
|
||||
|
||||
if (!FeedPagesSeen.containsKey(fs)) {
|
||||
FeedPagesSeen.put(fs, 0);
|
||||
FeedStoriesSeen.put(fs, 0);
|
||||
NbActivity.updateAllActivities(false);
|
||||
|
||||
pageNumber++;
|
||||
StoriesResponse apiResponse = apiManager.getStories(fs, pageNumber, order, filter);
|
||||
|
||||
if (! isStoryResponseGood(apiResponse)) return;
|
||||
|
||||
// if any reading activities happened during the API call, the result is now stale.
|
||||
// discard it and start again
|
||||
if (dbHelper.getActions(false).getCount() > 0) return;
|
||||
|
||||
FeedPagesSeen.put(fs, pageNumber);
|
||||
totalStoriesSeen += apiResponse.stories.length;
|
||||
FeedStoriesSeen.put(fs, totalStoriesSeen);
|
||||
|
||||
insertStories(apiResponse);
|
||||
NbActivity.updateAllActivities(true);
|
||||
|
||||
if (apiResponse.stories.length == 0) {
|
||||
ExhaustedFeeds.add(fs);
|
||||
finished = true;
|
||||
return;
|
||||
}
|
||||
int pageNumber = FeedPagesSeen.get(fs);
|
||||
int totalStoriesSeen = FeedStoriesSeen.get(fs);
|
||||
|
||||
StoryOrder order = PrefsUtils.getStoryOrder(this, fs);
|
||||
ReadFilter filter = PrefsUtils.getReadFilter(this, fs);
|
||||
|
||||
pageloop: while (totalStoriesSeen < PendingFeeds.get(fs)) {
|
||||
if (stopSync()) return;
|
||||
|
||||
pageNumber++;
|
||||
StoriesResponse apiResponse = apiManager.getStories(fs, pageNumber, order, filter);
|
||||
|
||||
if (! isStoryResponseGood(apiResponse)) break feedloop;
|
||||
|
||||
FeedPagesSeen.put(fs, pageNumber);
|
||||
totalStoriesSeen += apiResponse.stories.length;
|
||||
FeedStoriesSeen.put(fs, totalStoriesSeen);
|
||||
|
||||
dbHelper.insertStories(apiResponse);
|
||||
|
||||
if (apiResponse.stories.length == 0) {
|
||||
ExhaustedFeeds.add(fs);
|
||||
break pageloop;
|
||||
}
|
||||
}
|
||||
|
||||
handledFeeds.add(fs);
|
||||
}
|
||||
finished = true;
|
||||
|
||||
PendingFeeds.keySet().removeAll(handledFeeds);
|
||||
} finally {
|
||||
if (StorySyncRunning) {
|
||||
StorySyncRunning = false;
|
||||
NbActivity.updateAllActivities();
|
||||
NbActivity.updateAllActivities(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void prefetchImages() {
|
||||
if (!PrefsUtils.isImagePrefetchEnabled(this)) return;
|
||||
try {
|
||||
while (ImageQueue.size() > 0) {
|
||||
ImagePrefetchRunning = true;
|
||||
NbActivity.updateAllActivities();
|
||||
|
||||
Set<String> fetchedImages = new HashSet<String>();
|
||||
Set<String> batch = new HashSet<String>(AppConstants.IMAGE_PREFETCH_BATCH_SIZE);
|
||||
batchloop: for (String url : ImageQueue) {
|
||||
batch.add(url);
|
||||
if (batch.size() >= AppConstants.IMAGE_PREFETCH_BATCH_SIZE) break batchloop;
|
||||
}
|
||||
try {
|
||||
for (String url : batch) {
|
||||
if (stopSync()) return;
|
||||
if (HoldStories) return;
|
||||
|
||||
imageCache.cacheImage(url);
|
||||
|
||||
fetchedImages.add(url);
|
||||
}
|
||||
} finally {
|
||||
ImageQueue.removeAll(fetchedImages);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (ImagePrefetchRunning) {
|
||||
ImagePrefetchRunning = false;
|
||||
NbActivity.updateAllActivities();
|
||||
synchronized (PENDING_FEED_MUTEX) {
|
||||
if (finished && fs.equals(PendingFeed)) PendingFeed = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -617,12 +539,47 @@ public class NBSyncService extends Service {
|
|||
return true;
|
||||
}
|
||||
|
||||
private boolean stopSync() {
|
||||
if (HaltNow) return true;
|
||||
if (!NetworkUtils.isOnline(this)) return true;
|
||||
void insertStories(StoriesResponse apiResponse) {
|
||||
dbHelper.insertStories(apiResponse, ActMode, ModeCutoff);
|
||||
}
|
||||
|
||||
void incrementRunningChild() {
|
||||
synchronized (WAKELOCK_MUTEX) {
|
||||
wl.acquire();
|
||||
}
|
||||
}
|
||||
|
||||
void decrementRunningChild(int startId) {
|
||||
synchronized (WAKELOCK_MUTEX) {
|
||||
if (wl == null) return;
|
||||
if (wl.isHeld()) {
|
||||
wl.release();
|
||||
}
|
||||
// our wakelock reference counts. only stop the service if it is in the background and if
|
||||
// we are the last thread to release the lock.
|
||||
if (!wl.isHeld()) {
|
||||
if (NbActivity.getActiveActivityCount() < 1) {
|
||||
stopSelf(startId);
|
||||
}
|
||||
lastStartIdCompleted = startId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean stopSync(Context context) {
|
||||
if (HaltNow) {
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(NBSyncService.class.getName(), "stopping sync, soft interrupt set.");
|
||||
return true;
|
||||
}
|
||||
if (context == null) return false;
|
||||
if (!NetworkUtils.isOnline(context)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean stopSync() {
|
||||
return stopSync(this);
|
||||
}
|
||||
|
||||
public void onTrimMemory (int level) {
|
||||
if (level > ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
|
||||
isMemoryLow = true;
|
||||
|
@ -639,25 +596,25 @@ public class NBSyncService extends Service {
|
|||
* Is the main feed/folder list sync running?
|
||||
*/
|
||||
public static boolean isFeedFolderSyncRunning() {
|
||||
return (ActionsRunning || FFSyncRunning || CleanupRunning || UnreadSyncRunning || StorySyncRunning || OriginalTextSyncRunning || ImagePrefetchRunning);
|
||||
return (HousekeepingRunning || ActionsRunning || FFSyncRunning || CleanupRunning || UnreadsService.running() || StorySyncRunning || OriginalTextService.running() || ImagePrefetchService.running());
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there a sync for a given FeedSet running?
|
||||
*/
|
||||
public static boolean isFeedSetSyncing(FeedSet fs) {
|
||||
return (PendingFeeds.containsKey(fs) && StorySyncRunning);
|
||||
public static boolean isFeedSetSyncing(FeedSet fs, Context context) {
|
||||
return (fs.equals(PendingFeed) && (!stopSync(context)));
|
||||
}
|
||||
|
||||
public static String getSyncStatusMessage() {
|
||||
if (ActionsRunning) return "Catching up reading actions . . .";
|
||||
if (FFSyncRunning) return "Syncing feeds . . .";
|
||||
if (CleanupRunning) return "Cleaning up storage . . .";
|
||||
if (UnreadHashSyncRunning) return "Syncing unread status . . .";
|
||||
if (UnreadSyncRunning) return "Syncing " + StoryHashQueue.size() + " stories . . .";
|
||||
if (ImagePrefetchRunning) return "Caching " + ImageQueue.size() + " images . . .";
|
||||
if (StorySyncRunning) return "Syncing stories . . .";
|
||||
if (OriginalTextSyncRunning) return "Syncing text for " + OriginalTextQueue.size() + " stories. . .";
|
||||
public static String getSyncStatusMessage(Context context) {
|
||||
if (HousekeepingRunning) return context.getResources().getString(R.string.sync_status_housekeeping);
|
||||
if (ActionsRunning) return context.getResources().getString(R.string.sync_status_actions);
|
||||
if (FFSyncRunning) return context.getResources().getString(R.string.sync_status_ffsync);
|
||||
if (CleanupRunning) return context.getResources().getString(R.string.sync_status_cleanup);
|
||||
if (StorySyncRunning) return context.getResources().getString(R.string.sync_status_stories);
|
||||
if (UnreadsService.running()) return String.format(context.getResources().getString(R.string.sync_status_unreads), UnreadsService.getPendingCount());
|
||||
if (OriginalTextService.running()) return String.format(context.getResources().getString(R.string.sync_status_text), OriginalTextService.getPendingCount());
|
||||
if (ImagePrefetchService.running()) return String.format(context.getResources().getString(R.string.sync_status_images), ImagePrefetchService.getPendingCount());
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -670,12 +627,15 @@ public class NBSyncService extends Service {
|
|||
}
|
||||
|
||||
/**
|
||||
* Indicates that now is *not* an appropriate time to modify the story list because the user is
|
||||
* actively seeing stories. Only updates and appends should be performed, not cleanup or
|
||||
* a pagination reset.
|
||||
* Tell the service which stories can be activated if received. See ActivationMode.
|
||||
*/
|
||||
public static void holdStories(boolean holdStories) {
|
||||
HoldStories = holdStories;
|
||||
public static void setActivationMode(ActivationMode actMode) {
|
||||
ActMode = actMode;
|
||||
}
|
||||
|
||||
public static void setActivationMode(ActivationMode actMode, long modeCutoff) {
|
||||
ActMode = actMode;
|
||||
ModeCutoff = modeCutoff;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -692,31 +652,39 @@ public class NBSyncService extends Service {
|
|||
return false;
|
||||
}
|
||||
|
||||
synchronized (PendingFeeds) {
|
||||
synchronized (PENDING_FEED_MUTEX) {
|
||||
Integer alreadyPending = 0;
|
||||
if (fs.equals(PendingFeed)) alreadyPending = PendingFeedTarget;
|
||||
Integer alreadySeen = FeedStoriesSeen.get(fs);
|
||||
Integer alreadyRequested = PendingFeeds.get(fs);
|
||||
if (alreadySeen == null) alreadySeen = 0;
|
||||
if (alreadyRequested == null) alreadyRequested = 0;
|
||||
if ((callerSeen >= 0) && (alreadySeen > callerSeen)) {
|
||||
if (callerSeen < alreadySeen) {
|
||||
// the caller is probably filtering and thinks they have fewer than we do, so
|
||||
// update our count to agree with them, and force-allow another requet
|
||||
alreadySeen = callerSeen;
|
||||
FeedStoriesSeen.put(fs, callerSeen);
|
||||
alreadyRequested = 0;
|
||||
alreadyPending = 0;
|
||||
}
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(NBSyncService.class.getName(), "have:" + alreadySeen + " want:" + desiredStoryCount + " requested:" + alreadyRequested);
|
||||
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(NBSyncService.class.getName(), "have:" + alreadySeen + " want:" + desiredStoryCount + " pending:" + alreadyPending);
|
||||
if (desiredStoryCount <= alreadySeen) {
|
||||
return false;
|
||||
}
|
||||
if (desiredStoryCount <= alreadyRequested) {
|
||||
if (desiredStoryCount <= alreadyPending) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
PendingFeeds.put(fs, desiredStoryCount);
|
||||
PendingFeed = fs;
|
||||
PendingFeedTarget = desiredStoryCount;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void clearPendingStoryRequest() {
|
||||
synchronized (PENDING_FEED_MUTEX) {
|
||||
PendingFeed = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void resetFeeds() {
|
||||
ExhaustedFeeds.clear();
|
||||
FeedPagesSeen.clear();
|
||||
|
@ -724,11 +692,11 @@ public class NBSyncService extends Service {
|
|||
}
|
||||
|
||||
public static void getOriginalText(String hash) {
|
||||
OriginalTextQueue.add(hash);
|
||||
OriginalTextService.addHash(hash);
|
||||
}
|
||||
|
||||
public static void softInterrupt() {
|
||||
Log.d(NBSyncService.class.getName(), "soft stop");
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(NBSyncService.class.getName(), "soft stop");
|
||||
HaltNow = true;
|
||||
}
|
||||
|
||||
|
@ -738,23 +706,25 @@ public class NBSyncService extends Service {
|
|||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onDestroy - stopping execution");
|
||||
HaltNow = true;
|
||||
executor.shutdown();
|
||||
boolean cleanShutdown = false;
|
||||
try {
|
||||
cleanShutdown = executor.awaitTermination(60, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
// this value is somewhat arbitrary. ideally we would wait the max network timeout, but
|
||||
// the system like to force-kill terminating services that take too long, so it is often
|
||||
// moot to tune.
|
||||
executor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onDestroy - stopping execution");
|
||||
HaltNow = true;
|
||||
unreadsService.shutdown();
|
||||
originalTextService.shutdown();
|
||||
imagePrefetchService.shutdown();
|
||||
primaryExecutor.shutdown();
|
||||
try {
|
||||
primaryExecutor.awaitTermination(AppConstants.SHUTDOWN_SLACK_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
primaryExecutor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
dbHelper.close();
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onDestroy - execution halted");
|
||||
super.onDestroy();
|
||||
} catch (Exception ex) {
|
||||
Log.e(this.getClass().getName(), "unclean shutdown", ex);
|
||||
}
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onDestroy - execution halted");
|
||||
|
||||
super.onDestroy();
|
||||
dbHelper.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
package com.newsblur.service;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.newsblur.network.domain.StoryTextResponse;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class OriginalTextService extends SubService {
|
||||
|
||||
private static volatile boolean Running = false;
|
||||
|
||||
/** story hashes we need to fetch (from newly found stories) */
|
||||
private static Set<String> Hashes;
|
||||
static {Hashes = new HashSet<String>();}
|
||||
/** story hashes we should fetch ASAP (they are waiting on-screen) */
|
||||
private static Set<String> PriorityHashes;
|
||||
static {PriorityHashes = new HashSet<String>();}
|
||||
|
||||
public OriginalTextService(NBSyncService parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void exec() {
|
||||
while ((Hashes.size() > 0) || (PriorityHashes.size() > 0)) {
|
||||
if (parent.stopSync()) return;
|
||||
gotWork();
|
||||
fetchBatch(PriorityHashes);
|
||||
fetchBatch(Hashes);
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchBatch(Set<String> hashes) {
|
||||
Set<String> fetchedHashes = new HashSet<String>();
|
||||
Set<String> batch = new HashSet<String>(AppConstants.IMAGE_PREFETCH_BATCH_SIZE);
|
||||
batchloop: for (String hash : hashes) {
|
||||
batch.add(hash);
|
||||
if (batch.size() >= AppConstants.IMAGE_PREFETCH_BATCH_SIZE) break batchloop;
|
||||
}
|
||||
try {
|
||||
fetchloop: for (String hash : batch) {
|
||||
if (parent.stopSync()) return;
|
||||
String result = "";
|
||||
StoryTextResponse response = parent.apiManager.getStoryText(FeedUtils.inferFeedId(hash), hash);
|
||||
if ((response != null) && (response.originalText != null)) {
|
||||
result = response.originalText;
|
||||
}
|
||||
parent.dbHelper.putStoryText(hash, result);
|
||||
fetchedHashes.add(hash);
|
||||
}
|
||||
} finally {
|
||||
gotData();
|
||||
hashes.removeAll(fetchedHashes);
|
||||
}
|
||||
}
|
||||
|
||||
public static void addHash(String hash) {
|
||||
Hashes.add(hash);
|
||||
}
|
||||
|
||||
public static void addPriorityHash(String hash) {
|
||||
PriorityHashes.add(hash);
|
||||
}
|
||||
|
||||
public static int getPendingCount() {
|
||||
return (Hashes.size() + PriorityHashes.size());
|
||||
}
|
||||
|
||||
public static boolean running() {
|
||||
return Running;
|
||||
}
|
||||
@Override
|
||||
protected void setRunning(boolean running) {
|
||||
Running = running;
|
||||
}
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return Running;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
package com.newsblur.service;
|
||||
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
|
||||
import com.newsblur.activity.NbActivity;
|
||||
import com.newsblur.util.AppConstants;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* A utility construct to make NbSyncService a bit more modular by encapsulating sync tasks
|
||||
* that can be run fully asynchronously from the main sync loop. Like all of the sync service,
|
||||
* flags and data used by these modules need to be static so that parts of the app without a
|
||||
* handle to the service object can access them.
|
||||
*/
|
||||
public abstract class SubService {
|
||||
|
||||
protected NBSyncService parent;
|
||||
private ExecutorService executor;
|
||||
protected int startId;
|
||||
private long cycleStartTime = 0L;
|
||||
|
||||
private SubService() {
|
||||
; // no default construction
|
||||
}
|
||||
|
||||
SubService(NBSyncService parent) {
|
||||
this.parent = parent;
|
||||
executor = Executors.newFixedThreadPool(1);
|
||||
}
|
||||
|
||||
public void start(final int startId) {
|
||||
parent.incrementRunningChild();
|
||||
this.startId = startId;
|
||||
Runnable r = new Runnable() {
|
||||
public void run() {
|
||||
if (NbActivity.getActiveActivityCount() < 1) {
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_LESS_FAVORABLE );
|
||||
} else {
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT + Process.THREAD_PRIORITY_LESS_FAVORABLE + Process.THREAD_PRIORITY_LESS_FAVORABLE );
|
||||
}
|
||||
exec_();
|
||||
parent.decrementRunningChild(startId);
|
||||
}
|
||||
};
|
||||
executor.execute(r);
|
||||
}
|
||||
|
||||
private synchronized void exec_() {
|
||||
try {
|
||||
//if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService started");
|
||||
exec();
|
||||
//if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService completed");
|
||||
cycleStartTime = 0;
|
||||
} catch (Exception e) {
|
||||
Log.e(this.getClass().getName(), "Sync error.", e);
|
||||
} finally {
|
||||
if (isRunning()) {
|
||||
setRunning(false);
|
||||
NbActivity.updateAllActivities(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void exec();
|
||||
|
||||
public void shutdown() {
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService stopping");
|
||||
executor.shutdown();
|
||||
try {
|
||||
executor.awaitTermination(AppConstants.SHUTDOWN_SLACK_SECONDS, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
executor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService stopped");
|
||||
}
|
||||
}
|
||||
|
||||
protected void gotWork() {
|
||||
setRunning(true);
|
||||
NbActivity.updateAllActivities(false);
|
||||
}
|
||||
|
||||
protected void gotData() {
|
||||
NbActivity.updateAllActivities(true);
|
||||
}
|
||||
|
||||
protected abstract void setRunning(boolean running);
|
||||
protected abstract boolean isRunning();
|
||||
|
||||
/**
|
||||
* If called at the beginning of an expensive loop in a SubService, enforces the maximum duty cycle
|
||||
* defined in AppConstants by sleeping for a short while so the SubService does not dominate system
|
||||
* resources.
|
||||
*/
|
||||
protected void startExpensiveCycle() {
|
||||
if (cycleStartTime == 0) {
|
||||
cycleStartTime = System.nanoTime();
|
||||
return;
|
||||
}
|
||||
|
||||
double lastCycleTime = (System.nanoTime() - cycleStartTime);
|
||||
if (lastCycleTime < 1) return;
|
||||
|
||||
cycleStartTime = System.nanoTime();
|
||||
|
||||
double cooloffTime = lastCycleTime * (1.0 - AppConstants.MAX_BG_DUTY_CYCLE);
|
||||
if (cooloffTime < 1) return;
|
||||
long cooloffTimeMs = Math.round(cooloffTime / 1000000.0);
|
||||
if (cooloffTimeMs > AppConstants.DUTY_CYCLE_BACKOFF_CAP_MILLIS) cooloffTimeMs = AppConstants.DUTY_CYCLE_BACKOFF_CAP_MILLIS;
|
||||
|
||||
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "Sleeping for : " + cooloffTimeMs + "ms to enforce max duty cycle.");
|
||||
try {
|
||||
Thread.sleep(cooloffTimeMs);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
package com.newsblur.service;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.network.domain.StoriesResponse;
|
||||
import com.newsblur.network.domain.UnreadStoryHashesResponse;
|
||||
import com.newsblur.util.AppConstants;
|
||||
import com.newsblur.util.DefaultFeedView;
|
||||
import com.newsblur.util.FeedUtils;
|
||||
import com.newsblur.util.PrefsUtils;
|
||||
import com.newsblur.util.StoryOrder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.NavigableMap;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public class UnreadsService extends SubService {
|
||||
|
||||
private static volatile boolean Running = false;
|
||||
|
||||
private static volatile boolean doMetadata = false;
|
||||
|
||||
/** Unread story hashes the API listed that we do not appear to have locally yet. */
|
||||
private static List<String> StoryHashQueue;
|
||||
static { StoryHashQueue = new ArrayList<String>(); }
|
||||
|
||||
public UnreadsService(NBSyncService parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void exec() {
|
||||
// only use the unread status API if the user is premium
|
||||
if (parent.isPremium != Boolean.TRUE) return;
|
||||
|
||||
if (doMetadata) {
|
||||
gotWork();
|
||||
syncUnreadList();
|
||||
doMetadata = false;
|
||||
}
|
||||
|
||||
if (StoryHashQueue.size() < 1) return;
|
||||
|
||||
getNewUnreadStories();
|
||||
}
|
||||
|
||||
private void syncUnreadList() {
|
||||
// a self-sorting map with keys based upon the timestamp of the story returned by
|
||||
// the unreads API, concatenated with the hash to disambiguate duplicate timestamps.
|
||||
// values are the actual story hash, which will be extracted once we have processed
|
||||
// all hashes.
|
||||
NavigableMap<String,String> sortingMap = new TreeMap<String,String>();
|
||||
UnreadStoryHashesResponse unreadHashes = parent.apiManager.getUnreadStoryHashes();
|
||||
|
||||
// note all the stories we thought were unread before. if any fail to appear in
|
||||
// the API request for unreads, we will mark them as read
|
||||
List<String> oldUnreadHashes = parent.dbHelper.getUnreadStoryHashes();
|
||||
|
||||
// process the api response, both bookkeeping no-longer-unread stories and populating
|
||||
// the sortation map we will use to create the fetch list for step two
|
||||
for (Entry<String, List<String[]>> entry : unreadHashes.unreadHashes.entrySet()) {
|
||||
String feedId = entry.getKey();
|
||||
// ignore unreads from orphaned feeds
|
||||
if( ! parent.orphanFeedIds.contains(feedId)) {
|
||||
// only fetch the reported unreads if we don't already have them
|
||||
List<String> existingHashes = parent.dbHelper.getStoryHashesForFeed(feedId);
|
||||
for (String[] newHash : entry.getValue()) {
|
||||
if (!existingHashes.contains(newHash[0])) {
|
||||
sortingMap.put(newHash[1]+newHash[0], newHash[0]);
|
||||
}
|
||||
oldUnreadHashes.remove(newHash[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now that we have the sorted set of hashes, turn them into a list over which we
|
||||
// can iterate to fetch them
|
||||
if (PrefsUtils.getDefaultStoryOrder(parent) == StoryOrder.NEWEST) {
|
||||
// if the user reads newest-first by default, reverse the download order
|
||||
sortingMap = sortingMap.descendingMap();
|
||||
}
|
||||
StoryHashQueue.clear();
|
||||
for (Map.Entry<String,String> entry : sortingMap.entrySet()) {
|
||||
StoryHashQueue.add(entry.getValue());
|
||||
}
|
||||
|
||||
// any stories that we previously thought to be unread but were not found in the
|
||||
// list, mark them read now
|
||||
parent.dbHelper.markStoryHashesRead(oldUnreadHashes);
|
||||
}
|
||||
|
||||
private void getNewUnreadStories() {
|
||||
unreadsyncloop: while (StoryHashQueue.size() > 0) {
|
||||
if (parent.stopSync()) return;
|
||||
if(!PrefsUtils.isOfflineEnabled(parent)) return;
|
||||
gotWork();
|
||||
startExpensiveCycle();
|
||||
|
||||
List<String> hashBatch = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE);
|
||||
batchloop: for (String hash : StoryHashQueue) {
|
||||
hashBatch.add(hash);
|
||||
if (hashBatch.size() >= AppConstants.UNREAD_FETCH_BATCH_SIZE) break batchloop;
|
||||
}
|
||||
StoriesResponse response = parent.apiManager.getStoriesByHash(hashBatch);
|
||||
if (! isStoryResponseGood(response)) {
|
||||
Log.e(this.getClass().getName(), "error fetching unreads batch, abandoning sync.");
|
||||
break unreadsyncloop;
|
||||
}
|
||||
parent.insertStories(response);
|
||||
for (String hash : hashBatch) {
|
||||
StoryHashQueue.remove(hash);
|
||||
}
|
||||
|
||||
for (Story story : response.stories) {
|
||||
if (story.imageUrls != null) {
|
||||
for (String url : story.imageUrls) {
|
||||
parent.imagePrefetchService.addUrl(url);
|
||||
}
|
||||
}
|
||||
DefaultFeedView mode = PrefsUtils.getDefaultFeedViewForFeed(parent, story.feedId);
|
||||
if (mode == DefaultFeedView.TEXT) {
|
||||
parent.originalTextService.addHash(story.storyHash);
|
||||
}
|
||||
}
|
||||
parent.originalTextService.start(startId);
|
||||
parent.imagePrefetchService.start(startId);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStoryResponseGood(StoriesResponse response) {
|
||||
if (response == null) {
|
||||
Log.e(this.getClass().getName(), "Null response received while loading stories.");
|
||||
return false;
|
||||
}
|
||||
if (response.stories == null) {
|
||||
Log.e(this.getClass().getName(), "Null stories member received while loading stories.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void clearHashes() {
|
||||
StoryHashQueue.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe the number of unreads left to be synced or return an empty message (space padded).
|
||||
*/
|
||||
public static String getPendingCount() {
|
||||
int c = StoryHashQueue.size();
|
||||
if (c < 1) {
|
||||
return " ";
|
||||
} else {
|
||||
return " " + c + " ";
|
||||
}
|
||||
}
|
||||
|
||||
public static void doMetadata() {
|
||||
doMetadata = true;
|
||||
}
|
||||
|
||||
public static boolean running() {
|
||||
return Running;
|
||||
}
|
||||
@Override
|
||||
protected void setRunning(boolean running) {
|
||||
Running = running;
|
||||
}
|
||||
@Override
|
||||
protected boolean isRunning() {
|
||||
return Running;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -30,6 +30,9 @@ public class AppConstants {
|
|||
// how long to wait before auto-syncing the feed/folder list
|
||||
public static final long AUTO_SYNC_TIME_MILLIS = 15L * 60L * 1000L;
|
||||
|
||||
// how often to rebuild the DB
|
||||
public static final long VACUUM_TIME_MILLIS = 24L * 60L * 60L * 1000L;
|
||||
|
||||
// how often to trigger the BG service. slightly longer than how often we will find new stories,
|
||||
// to account for the fact that it is approximate, and missing a cycle is bad.
|
||||
public static final long BG_SERVICE_CYCLE_MILLIS = AUTO_SYNC_TIME_MILLIS + 30L * 1000L;
|
||||
|
@ -45,21 +48,33 @@ public class AppConstants {
|
|||
public static final int MAX_FEED_LIST_SIZE = 250;
|
||||
|
||||
// when reading stories, how many stories worth of buffer to keep loaded ahead of the user
|
||||
public static final int READING_STORY_PRELOAD = 5;
|
||||
public static final int READING_STORY_PRELOAD = 10;
|
||||
|
||||
// max old stories to keep in the DB per feed before fetching new unreads
|
||||
public static final int MAX_READ_STORIES_STORED = 500;
|
||||
|
||||
// how many unread stories to fetch via hash at a time
|
||||
public static final int UNREAD_FETCH_BATCH_SIZE = 50;
|
||||
public static final int UNREAD_FETCH_BATCH_SIZE = 100;
|
||||
|
||||
// how many images to prefetch before updating the countdown UI
|
||||
public static final int IMAGE_PREFETCH_BATCH_SIZE = 10;
|
||||
public static final int IMAGE_PREFETCH_BATCH_SIZE = 6;
|
||||
|
||||
// should the feedback link be enabled (read: is this a beta?)
|
||||
public static final boolean ENABLE_FEEDBACK = true;
|
||||
|
||||
// link to app feedback page
|
||||
public static final String FEEDBACK_URL = "https://getsatisfaction.com/newsblur/topics/new?topic[style]=question&from=company&product=NewsBlur+Android+App&topic[additional_detail]=";
|
||||
public static final String FEEDBACK_URL = "https://getsatisfaction.com/newsblur/topics/new/add_details?topic[subject]=Android%3A+&topic[categories][][id]=80957&topic[type]=question&topic[content]=";
|
||||
|
||||
// how long to wait for sync threads to shutdown. ideally we would wait the max network timeout,
|
||||
// but the system like to force-kill terminating services that take too long, so it is often
|
||||
// moot to tune.
|
||||
public final static long SHUTDOWN_SLACK_SECONDS = 60L;
|
||||
|
||||
// the maximum duty cycle for expensive background tasks. Tune to <1.0 to force sync loops
|
||||
// to pause periodically and not peg the network/CPU
|
||||
public final static double MAX_BG_DUTY_CYCLE = 0.9;
|
||||
|
||||
// cap duty cycle backoffs to prevent unnecessarily large backoffs
|
||||
public final static long DUTY_CYCLE_BACKOFF_CAP_MILLIS = 5L * 1000L;
|
||||
|
||||
}
|
||||
|
|
|
@ -8,12 +8,8 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.text.Html;
|
||||
import android.text.TextUtils;
|
||||
|
@ -23,13 +19,10 @@ import android.widget.Toast;
|
|||
import com.newsblur.R;
|
||||
import com.newsblur.activity.NbActivity;
|
||||
import com.newsblur.database.BlurDatabaseHelper;
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.database.FeedProvider;
|
||||
import com.newsblur.domain.Classifier;
|
||||
import com.newsblur.domain.Feed;
|
||||
import com.newsblur.domain.SocialFeed;
|
||||
import com.newsblur.domain.Story;
|
||||
import com.newsblur.domain.ValueMultimap;
|
||||
import com.newsblur.network.APIManager;
|
||||
import com.newsblur.network.domain.NewsBlurResponse;
|
||||
import com.newsblur.service.NBSyncService;
|
||||
|
@ -37,7 +30,7 @@ import com.newsblur.util.AppConstants;
|
|||
|
||||
public class FeedUtils {
|
||||
|
||||
private static BlurDatabaseHelper dbHelper;
|
||||
public static BlurDatabaseHelper dbHelper;
|
||||
|
||||
public static void offerDB(BlurDatabaseHelper _dbHelper) {
|
||||
if (_dbHelper.isOpen()) {
|
||||
|
@ -96,7 +89,21 @@ public class FeedUtils {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
}.execute();
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
public static void activateAllStories() {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground(Void... arg) {
|
||||
try {
|
||||
dbHelper.markStoriesActive(NBSyncService.ActivationMode.ALL, 0L);
|
||||
} catch (Exception e) {
|
||||
; // this call can evade the on-upgrade DB wipe and throw exceptions
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
public static void markStoryUnread(final Story story, final Context context) {
|
||||
|
@ -174,17 +181,9 @@ public class FeedUtils {
|
|||
|
||||
// next, update the local DB
|
||||
classifier.getMapForType(classifierType).put(key, classifierAction);
|
||||
Uri classifierUri = FeedProvider.CLASSIFIER_URI.buildUpon().appendPath(feedId).build();
|
||||
try {
|
||||
// TODO: for feeds with many classifiers, this could be much faster by targeting just the row that changed
|
||||
context.getContentResolver().delete(classifierUri, null, null);
|
||||
for (ContentValues classifierValues : classifier.getContentValues()) {
|
||||
context.getContentResolver().insert(classifierUri, classifierValues);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(FeedUtils.class.getName(), "Could not update classifier in local storage.", e);
|
||||
}
|
||||
|
||||
classifier.feedId = feedId;
|
||||
dbHelper.clearClassifiersForFeed(feedId);
|
||||
dbHelper.insertClassifier(classifier);
|
||||
}
|
||||
|
||||
public static void shareStory(Story story, Context context) {
|
||||
|
|
|
@ -7,6 +7,8 @@ import java.io.File;
|
|||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
@ -88,12 +90,19 @@ public class ImageCache {
|
|||
return fileName;
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
public void cleanup(Set<String> currentImages) {
|
||||
// if there appear to be zero images in the system, a DB rebuild probably just
|
||||
// occured, so don't trust that data for cleanup
|
||||
if (currentImages.size() == 0) return;
|
||||
|
||||
Set<String> currentFiles = new HashSet<String>(currentImages.size());
|
||||
for (String url : currentImages) currentFiles.add(getFileName(url));
|
||||
File[] files = cacheDir.listFiles();
|
||||
if (files == null) return;
|
||||
for (File f : files) {
|
||||
long timestamp = f.lastModified();
|
||||
if (System.currentTimeMillis() > (timestamp + MAX_FILE_AGE_MILLIS)) {
|
||||
if ((System.currentTimeMillis() > (timestamp + MAX_FILE_AGE_MILLIS)) ||
|
||||
(!currentFiles.contains(f.getName()))) {
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,4 +58,8 @@ public class PrefConstants {
|
|||
public static final String THEME = "theme";
|
||||
|
||||
public static final String STATE_FILTER = "state_filter";
|
||||
|
||||
public static final String LAST_VACUUM_TIME = "last_vacuum_time";
|
||||
|
||||
public static final String VOLUME_KEY_NAVIGATION = "volume_key_navigation";
|
||||
}
|
||||
|
|
|
@ -40,38 +40,32 @@ public class PrefsUtils {
|
|||
edit.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if this is the first launch of the app after an upgrade, in which case
|
||||
* we clear the DB to prevent bugs associated with non-forward-compatibility.
|
||||
*/
|
||||
public static void checkForUpgrade(Context context) {
|
||||
|
||||
public static boolean checkForUpgrade(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
|
||||
String version = getVersion(context);
|
||||
if (version == null) {
|
||||
Log.w(PrefsUtils.class.getName(), "could not determine app version");
|
||||
return;
|
||||
Log.wtf(PrefsUtils.class.getName(), "could not determine app version");
|
||||
return false;
|
||||
}
|
||||
Log.i(PrefsUtils.class.getName(), "launching version: " + version);
|
||||
if (AppConstants.VERBOSE_LOG) Log.i(PrefsUtils.class.getName(), "launching version: " + version);
|
||||
|
||||
String oldVersion = prefs.getString(AppConstants.LAST_APP_VERSION, null);
|
||||
if ( (oldVersion == null) || (!oldVersion.equals(version)) ) {
|
||||
Log.i(PrefsUtils.class.getName(), "detected new version of app, clearing local data");
|
||||
// wipe the local DB
|
||||
FeedUtils.dropAndRecreateTables();
|
||||
// in case this is the first time we have run since moving the cache to the new location,
|
||||
// blow away the old version entirely. This line can be removed some time well after
|
||||
// v61+ is widely deployed
|
||||
FileCache.cleanUpOldCache(context);
|
||||
// store the current version
|
||||
prefs.edit().putString(AppConstants.LAST_APP_VERSION, version).commit();
|
||||
// also make sure we auto-trigger an update, since all data are now gone
|
||||
prefs.edit().putLong(AppConstants.LAST_SYNC_TIME, 0L).commit();
|
||||
Log.i(PrefsUtils.class.getName(), "detected new version of app");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
public static void updateVersion(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
// store the current version
|
||||
prefs.edit().putString(AppConstants.LAST_APP_VERSION, getVersion(context)).commit();
|
||||
// also make sure we auto-trigger an update, since all data are now gone
|
||||
prefs.edit().putLong(AppConstants.LAST_SYNC_TIME, 0L).commit();
|
||||
}
|
||||
|
||||
public static String getVersion(Context context) {
|
||||
try {
|
||||
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
|
||||
|
@ -234,6 +228,17 @@ public class PrefsUtils {
|
|||
prefs.edit().putLong(AppConstants.LAST_SYNC_TIME, (new Date()).getTime()).commit();
|
||||
}
|
||||
|
||||
public static boolean isTimeToVacuum(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
long lastTime = prefs.getLong(PrefConstants.LAST_VACUUM_TIME, 1L);
|
||||
return ( (lastTime + AppConstants.VACUUM_TIME_MILLIS) < (new Date()).getTime() );
|
||||
}
|
||||
|
||||
public static void updateLastVacuumTime(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
prefs.edit().putLong(PrefConstants.LAST_VACUUM_TIME, (new Date()).getTime()).commit();
|
||||
}
|
||||
|
||||
public static StoryOrder getStoryOrderForFeed(Context context, String feedId) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
return StoryOrder.valueOf(prefs.getString(PrefConstants.FEED_STORY_ORDER_PREFIX + feedId, getDefaultStoryOrder(prefs).toString()));
|
||||
|
@ -285,6 +290,11 @@ public class PrefsUtils {
|
|||
private static StoryOrder getDefaultStoryOrder(SharedPreferences prefs) {
|
||||
return StoryOrder.valueOf(prefs.getString(PrefConstants.DEFAULT_STORY_ORDER, StoryOrder.NEWEST.toString()));
|
||||
}
|
||||
|
||||
public static StoryOrder getDefaultStoryOrder(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
return getDefaultStoryOrder(preferences);
|
||||
}
|
||||
|
||||
private static ReadFilter getDefaultReadFilter(SharedPreferences prefs) {
|
||||
return ReadFilter.valueOf(prefs.getString(PrefConstants.DEFAULT_READ_FILTER, ReadFilter.ALL.toString()));
|
||||
|
@ -475,4 +485,9 @@ public class PrefsUtils {
|
|||
editor.putString(PrefConstants.STATE_FILTER, newValue.toString());
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public static VolumeKeyNavigation getVolumeKeyNavigation(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||
return VolumeKeyNavigation.valueOf(prefs.getString(PrefConstants.VOLUME_KEY_NAVIGATION, VolumeKeyNavigation.OFF.toString()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package com.newsblur.util;
|
||||
|
||||
/**
|
||||
* Enum to represent preference value for using the volume key for next/prev story.
|
||||
* Created by mark on 28/01/15.
|
||||
*/
|
||||
public enum VolumeKeyNavigation {
|
||||
OFF("off"),
|
||||
UP_NEXT("up_next"),
|
||||
DOWN_NEXT("down_next");
|
||||
|
||||
private String parameterValue;
|
||||
|
||||
VolumeKeyNavigation(String parameterValue) {
|
||||
this.parameterValue = parameterValue;
|
||||
}
|
||||
|
||||
public String getParameterValue() {
|
||||
return parameterValue;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry kind="src" path="gen"/>
|
||||
<classpathentry combineaccessrules="false" kind="src" path="/NewsBlur"/>
|
||||
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
|
||||
<classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
|
||||
<classpathentry kind="output" path="bin/classes"/>
|
||||
</classpath>
|
|
@ -1,34 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>NewsBlurTest</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
<project>NewsBlur</project>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.newsblur.test"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0" >
|
||||
|
||||
<uses-sdk android:minSdkVersion="8" />
|
||||
|
||||
<instrumentation
|
||||
android:name="android.test.InstrumentationTestRunner"
|
||||
android:targetPackage="com.newsblur" />
|
||||
|
||||
<application
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name" >
|
||||
<uses-library android:name="android.test.runner" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -1,20 +0,0 @@
|
|||
# To enable ProGuard in your project, edit project.properties
|
||||
# to define the proguard.config property as described in that file.
|
||||
#
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in ${sdk.dir}/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the ProGuard
|
||||
# include property in project.properties.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
|
@ -1,14 +0,0 @@
|
|||
# This file is automatically generated by Android Tools.
|
||||
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
|
||||
#
|
||||
# This file must be checked in Version Control Systems.
|
||||
#
|
||||
# To customize properties used by the Ant build system edit
|
||||
# "ant.properties", and override values to adapt the script to your
|
||||
# project structure.
|
||||
#
|
||||
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
|
||||
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
|
||||
|
||||
# Project target.
|
||||
target=android-8
|
Before Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 14 KiB |
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hello" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="hello">Hello World!</string>
|
||||
<string name="app_name">NewsBlurTestTest</string>
|
||||
|
||||
</resources>
|
|
@ -1,116 +0,0 @@
|
|||
package com.newsblur.test.database;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.test.ProviderTestCase2;
|
||||
import android.util.Log;
|
||||
|
||||
import com.newsblur.database.BlurDatabase;
|
||||
import com.newsblur.database.DatabaseConstants;
|
||||
import com.newsblur.database.FeedProvider;
|
||||
import com.newsblur.domain.Folder;
|
||||
import com.newsblur.domain.Story;
|
||||
|
||||
public class FolderProviderTest extends ProviderTestCase2<FeedProvider> {
|
||||
|
||||
private BlurDatabase dbHelper;
|
||||
private ContentResolver contentResolver;
|
||||
private String TAG = "FolderProviderTest";
|
||||
|
||||
public FolderProviderTest() {
|
||||
super(FeedProvider.class, FeedProvider.AUTHORITY);
|
||||
}
|
||||
|
||||
public void testPrereqs() {
|
||||
assertNotNull(contentResolver);
|
||||
assertNotNull(dbHelper);
|
||||
}
|
||||
|
||||
public void testOnCreate() {
|
||||
assertTrue(getProvider().onCreate());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
Log.d(TAG , "Setup");
|
||||
|
||||
dbHelper = new BlurDatabase(getContext());
|
||||
dbHelper.dropAndRecreateTables();
|
||||
contentResolver = getContext().getContentResolver();
|
||||
}
|
||||
|
||||
public void testInsertFolder() {
|
||||
Folder testFolder = getTestFolder(1);
|
||||
|
||||
Uri uri = contentResolver.insert(FeedProvider.FOLDERS_URI, testFolder.values);
|
||||
SQLiteDatabase db = dbHelper.getReadableDatabase();
|
||||
Cursor rawQuery = db.rawQuery("SELECT * FROM " + DatabaseConstants.FOLDER_TABLE, null);
|
||||
|
||||
assertEquals(1, rawQuery.getCount());
|
||||
assertEquals("1", uri.getLastPathSegment());
|
||||
}
|
||||
|
||||
|
||||
public void testInsertStory() {
|
||||
Story testStory = getTestStory(1);
|
||||
Uri feedUri = FeedProvider.FEED_STORIES_URI.buildUpon().appendPath(testStory.feedId).build();
|
||||
|
||||
Uri uri = contentResolver.insert(feedUri, testStory.getValues());
|
||||
SQLiteDatabase db = dbHelper.getReadableDatabase();
|
||||
Cursor rawQuery = db.rawQuery("SELECT * FROM " + DatabaseConstants.STORY_TABLE, null);
|
||||
|
||||
assertEquals(1, rawQuery.getCount());
|
||||
}
|
||||
|
||||
|
||||
public void testInsertUpdateStory() {
|
||||
Story testStory = getTestStory(1);
|
||||
Uri feedUri = FeedProvider.FEED_STORIES_URI.buildUpon().appendPath(testStory.feedId).build();
|
||||
|
||||
contentResolver.insert(feedUri, testStory.getValues());
|
||||
SQLiteDatabase db = dbHelper.getReadableDatabase();
|
||||
Cursor rawQuery = db.rawQuery("SELECT * FROM " + DatabaseConstants.STORY_TABLE, null);
|
||||
|
||||
assertEquals(1, rawQuery.getCount());
|
||||
|
||||
testStory.read = 1;
|
||||
|
||||
contentResolver.insert(feedUri, testStory.getValues());
|
||||
|
||||
Cursor secondQuery = db.rawQuery("SELECT * FROM " + DatabaseConstants.STORY_TABLE, null);
|
||||
secondQuery.moveToFirst();
|
||||
assertEquals(1, secondQuery.getCount());
|
||||
assertEquals(1, secondQuery.getInt(secondQuery.getColumnIndex(DatabaseConstants.STORY_READ)));
|
||||
}
|
||||
|
||||
|
||||
private Folder getTestFolder(final int testId) {
|
||||
Folder folder = new Folder();
|
||||
folder.setId(Integer.toString(testId));
|
||||
folder.setName("Name" + testId);
|
||||
return folder;
|
||||
}
|
||||
|
||||
private Story getTestStory(final int testId) {
|
||||
Story story = new Story();
|
||||
story.authors = "Arthur Conan Doyle";
|
||||
story.commentCount = 1;
|
||||
story.content = "Watson, come here, I need you.";
|
||||
story.date = new Date();
|
||||
story.date.setTime(946747860000l); // January 1 2000, 12:30pm
|
||||
story.feedId = "3"; // Daring Fireball
|
||||
story.id = "The Story of the New iPhone";
|
||||
story.sharedUserIds = new String[] { };
|
||||
story.tags = new String[] { };
|
||||
story.permalink = "http://www.daringfireball.com/omgiphone";
|
||||
story.read = 0;
|
||||
story.title = "Hello";
|
||||
|
||||
return story;
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package com.newsblur.test.domain;
|
||||
|
||||
public class StoryTest {
|
||||
|
||||
}
|
|
@ -656,7 +656,7 @@
|
|||
NEWSBLUR_URL,
|
||||
storiesCollection.feedPage];
|
||||
} else {
|
||||
theFeedDetailURL = [NSString stringWithFormat:@"%@/reader/feed/%@/?page=%d",
|
||||
theFeedDetailURL = [NSString stringWithFormat:@"%@/reader/feed/%@/?include_hidden=true&page=%d",
|
||||
NEWSBLUR_URL,
|
||||
[storiesCollection.activeFeed objectForKey:@"id"],
|
||||
storiesCollection.feedPage];
|
||||
|
@ -867,7 +867,7 @@
|
|||
storiesCollection.feedPage];
|
||||
} else {
|
||||
theFeedDetailURL = [NSString stringWithFormat:
|
||||
@"%@/reader/river_stories/?f=%@&page=%d",
|
||||
@"%@/reader/river_stories/?include_hidden=true&f=%@&page=%d",
|
||||
NEWSBLUR_URL,
|
||||
[storiesCollection.activeFolderFeeds componentsJoinedByString:@"&f="],
|
||||
storiesCollection.feedPage];
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
-(id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(NSString *)activityType {
|
||||
if ([activityType isEqualToString:UIActivityTypeMail] ||
|
||||
[activityType isEqualToString:@"com.evernote.iPhone.Evernote.EvernoteShare"]) {
|
||||
return @{@"body": text, @"subject": title};
|
||||
return @{@"body": text ?: @"", @"subject": title};
|
||||
} else if ([activityType isEqualToString:UIActivityTypePostToTwitter] ||
|
||||
[activityType isEqualToString:UIActivityTypePostToFacebook] ||
|
||||
[activityType isEqualToString:UIActivityTypePostToWeibo]) {
|
||||
|
|
|
@ -909,7 +909,9 @@
|
|||
|
||||
- (IBAction)openSendToDialog:(id)sender {
|
||||
[self endTouchDown:sender];
|
||||
|
||||
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
|
||||
[appDelegate.masterContainerViewController showSendToPopover:sender];
|
||||
}
|
||||
[appDelegate showSendTo:self sender:sender];
|
||||
}
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
__weak __typeof(&*self)weakSelf = self;
|
||||
|
||||
[lock lock];
|
||||
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/reader/river_stories?page=0&h=%@",
|
||||
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/reader/river_stories?include_hidden=true&page=0&h=%@",
|
||||
NEWSBLUR_URL, [hashes componentsJoinedByString:@"&h="]]];
|
||||
if (request) request = nil;
|
||||
request = [AFJSONRequestOperation
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>4.6.2</string>
|
||||
<string>4.6.4</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
@ -58,7 +58,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>46</string>
|
||||
<string>48</string>
|
||||
<key>FacebookAppID</key>
|
||||
<string>230426707030569</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
|
|
@ -3866,7 +3866,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Distribution: NewsBlur, Inc.";
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -3893,7 +3893,7 @@
|
|||
"-all_load",
|
||||
);
|
||||
PRODUCT_NAME = NewsBlur;
|
||||
PROVISIONING_PROFILE = "0a21ce12-0604-4e59-b0b1-1aa2b90afaa5";
|
||||
PROVISIONING_PROFILE = "2f00e3a4-b7f2-4976-bb45-661087b33dc9";
|
||||
STRIP_INSTALLED_PRODUCT = NO;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
"WARNING_CFLAGS[arch=*]" = "-Wall";
|
||||
|
@ -3907,7 +3907,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Distribution: NewsBlur, Inc.";
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
COPY_PHASE_STRIP = YES;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -3932,7 +3932,7 @@
|
|||
"-all_load",
|
||||
);
|
||||
PRODUCT_NAME = NewsBlur;
|
||||
PROVISIONING_PROFILE = "0a21ce12-0604-4e59-b0b1-1aa2b90afaa5";
|
||||
PROVISIONING_PROFILE = "2f00e3a4-b7f2-4976-bb45-661087b33dc9";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
}
|
||||
|
||||
/var/log/haproxy*.log {
|
||||
su root root
|
||||
rotate 4
|
||||
weekly
|
||||
missingok
|
||||
|
|
|
@ -16,7 +16,7 @@ django-subdomains==2.0.3
|
|||
Django>=1.5,<1.6
|
||||
dop==0.1.4
|
||||
Fabric==1.8.3
|
||||
gunicorn==0.17.2
|
||||
gunicorn==19.1.1
|
||||
# psycopg2==2.5.2
|
||||
httplib2==0.8
|
||||
iconv==1.0
|
||||
|
@ -41,7 +41,7 @@ requests==2.3.0
|
|||
seacucumber==1.5
|
||||
South==0.7.6
|
||||
stripe==1.12.2
|
||||
django-oauth-toolkit==0.5.0
|
||||
django-oauth-toolkit==0.7.2
|
||||
django-cors-headers==0.12
|
||||
pyOpenSSL==0.14
|
||||
pyasn1==0.1.7
|
||||
|
|
43
fabfile.py
vendored
|
@ -195,7 +195,7 @@ def setup_common():
|
|||
setup_psql_client()
|
||||
setup_libxml()
|
||||
setup_python()
|
||||
# setup_psycopg()
|
||||
setup_psycopg()
|
||||
setup_supervisor()
|
||||
setup_hosts()
|
||||
config_pgbouncer()
|
||||
|
@ -600,7 +600,7 @@ def setup_sudoers(user=None):
|
|||
sudo('su - root -c "echo \\\\"%s ALL=(ALL) NOPASSWD: ALL\\\\" >> /etc/sudoers"' % (user or env.user))
|
||||
|
||||
def setup_nginx():
|
||||
NGINX_VERSION = '1.4.1'
|
||||
NGINX_VERSION = '1.6.2'
|
||||
with cd(env.VENDOR_PATH), settings(warn_only=True):
|
||||
sudo("groupadd nginx")
|
||||
sudo("useradd -g nginx -d /var/www/htdocs -s /bin/false nginx")
|
||||
|
@ -642,15 +642,19 @@ def setup_app_firewall():
|
|||
def setup_app_motd():
|
||||
put('config/motd_app.txt', '/etc/motd.tail', use_sudo=True)
|
||||
|
||||
def remove_gunicorn():
|
||||
with cd(env.VENDOR_PATH):
|
||||
sudo('rm -fr gunicorn')
|
||||
|
||||
def setup_gunicorn(supervisor=True):
|
||||
if supervisor:
|
||||
put('config/supervisor_gunicorn.conf', '/etc/supervisor/conf.d/gunicorn.conf', use_sudo=True)
|
||||
with cd(env.VENDOR_PATH):
|
||||
sudo('rm -fr gunicorn')
|
||||
run('git clone git://github.com/benoitc/gunicorn.git')
|
||||
with cd(os.path.join(env.VENDOR_PATH, 'gunicorn')):
|
||||
run('git pull')
|
||||
sudo('python setup.py develop')
|
||||
# with cd(env.VENDOR_PATH):
|
||||
# sudo('rm -fr gunicorn')
|
||||
# run('git clone git://github.com/benoitc/gunicorn.git')
|
||||
# with cd(os.path.join(env.VENDOR_PATH, 'gunicorn')):
|
||||
# run('git pull')
|
||||
# sudo('python setup.py develop')
|
||||
|
||||
|
||||
def update_gunicorn():
|
||||
|
@ -914,7 +918,7 @@ def setup_mongo_mms():
|
|||
sudo('start mongodb-mms-monitoring-agent')
|
||||
|
||||
def setup_redis(slave=False):
|
||||
redis_version = '2.6.16'
|
||||
redis_version = '2.8.19'
|
||||
with cd(env.VENDOR_PATH):
|
||||
run('wget http://download.redis.io/releases/redis-%s.tar.gz' % redis_version)
|
||||
run('tar -xzf redis-%s.tar.gz' % redis_version)
|
||||
|
@ -1019,7 +1023,7 @@ def setup_original_page_server():
|
|||
sudo('supervisorctl reload')
|
||||
|
||||
def setup_elasticsearch():
|
||||
ES_VERSION = "0.90.0"
|
||||
ES_VERSION = "0.90.13"
|
||||
sudo('apt-get update')
|
||||
sudo('apt-get install openjdk-7-jre -y')
|
||||
|
||||
|
@ -1028,7 +1032,7 @@ def setup_elasticsearch():
|
|||
with cd(os.path.join(env.VENDOR_PATH, 'elasticsearch-%s' % ES_VERSION)):
|
||||
run('wget http://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-%s.deb' % ES_VERSION)
|
||||
sudo('dpkg -i elasticsearch-%s.deb' % ES_VERSION)
|
||||
sudo('/usr/share/elasticsearch/bin/plugin -install mobz/elasticsearch-head' % ES_VERSION)
|
||||
sudo('/usr/share/elasticsearch/bin/plugin -install mobz/elasticsearch-head')
|
||||
|
||||
def setup_db_search():
|
||||
put('config/supervisor_celeryd_search_indexer.conf', '/etc/supervisor/conf.d/celeryd_search_indexer.conf', use_sudo=True)
|
||||
|
@ -1093,8 +1097,9 @@ def setup_do(name, size=2, image=None):
|
|||
ssh_key_ids = [str(k.id) for k in doapi.all_ssh_keys()]
|
||||
region_id = doapi.regions()[0].id
|
||||
if not image:
|
||||
IMAGE_NAME = "Ubuntu 13.10 x64"
|
||||
IMAGE_NAME = "14.04 x64"
|
||||
images = dict((s.name, s.id) for s in doapi.images())
|
||||
print images
|
||||
image_id = images[IMAGE_NAME]
|
||||
else:
|
||||
IMAGE_NAME = image
|
||||
|
@ -1214,8 +1219,8 @@ def post_deploy():
|
|||
cleanup_assets()
|
||||
|
||||
@parallel
|
||||
def deploy(fast=False):
|
||||
deploy_code(copy_assets=False, fast=fast)
|
||||
def deploy(fast=False, reload=False):
|
||||
deploy_code(copy_assets=False, fast=fast, reload=reload)
|
||||
|
||||
@parallel
|
||||
def deploy_web(fast=False):
|
||||
|
@ -1231,7 +1236,7 @@ def kill_gunicorn():
|
|||
sudo('pkill -9 -u %s -f gunicorn_django' % env.user)
|
||||
|
||||
@parallel
|
||||
def deploy_code(copy_assets=False, full=False, fast=False):
|
||||
def deploy_code(copy_assets=False, full=False, fast=False, reload=False):
|
||||
with cd(env.NEWSBLUR_PATH):
|
||||
run('git pull')
|
||||
run('mkdir -p static')
|
||||
|
@ -1239,9 +1244,13 @@ def deploy_code(copy_assets=False, full=False, fast=False):
|
|||
run('rm -fr static/*')
|
||||
if copy_assets:
|
||||
transfer_assets()
|
||||
sudo('supervisorctl reload')
|
||||
if fast:
|
||||
|
||||
if reload:
|
||||
sudo('supervisorctl reload')
|
||||
elif fast:
|
||||
kill_gunicorn()
|
||||
else:
|
||||
sudo('kill -HUP `cat /srv/newsblur/logs/gunicorn.pid`')
|
||||
|
||||
@parallel
|
||||
def kill():
|
||||
|
|
|
@ -18,6 +18,14 @@ ul.segmented-control {
|
|||
box-shadow: 0 1px 0 rgba(255, 255, 255, .15);
|
||||
}
|
||||
|
||||
.segmented-control ::-moz-selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.segmented-control ::selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.segmented-control li {
|
||||
background: none;
|
||||
float: left;
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
-webkit-box-shadow: 8px 8px 15px #505050;
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 2px, rgba(255, 255, 255, 0.4) 0px 0px 300px 0px;
|
||||
border: 1px solid rgba(5, 5, 5,.6);
|
||||
min-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* =================== */
|
||||
|
|
|
@ -478,8 +478,8 @@ a img {
|
|||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
.NB-theme-feed-size-xs .NB-feedlist {
|
||||
font-size: 10px;
|
||||
|
@ -599,7 +599,8 @@ a img {
|
|||
top: 2px;
|
||||
display: none;
|
||||
}
|
||||
#feed_list .feed.NB-feed-exception .NB-feed-exception-icon {
|
||||
#feed_list .feed.NB-feed-exception .NB-feed-exception-icon,
|
||||
.NB-modal-organizer .feed.NB-feed-exception .NB-feed-exception-icon {
|
||||
display: block;
|
||||
}
|
||||
.NB-feedlist .feed .NB-feed-highlight {
|
||||
|
@ -612,7 +613,7 @@ a img {
|
|||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
.NB-feedlist .feed.NB-feed-unfetched .feed_counts {
|
||||
.NB-feedlist .feed.NB-feed-unfetched:not(.NB-highlighted) .feed_counts {
|
||||
display: none;
|
||||
}
|
||||
.NB-feedlist .feed.NB-feed-unfetched .feed_favicon {
|
||||
|
@ -853,23 +854,6 @@ a img {
|
|||
border: none !important;
|
||||
}
|
||||
|
||||
.NB-feedlists .folder_title .unread_count {
|
||||
margin-top: -3px;
|
||||
line-height: 15px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.NB-theme-feed-size-l .NB-feedlists .folder_title .unread_count {
|
||||
padding-top: 3px;
|
||||
}
|
||||
.NB-theme-feed-size-xl .NB-feedlists .folder_title .unread_count {
|
||||
margin-top: -2px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
.NB-feeds-header .unread_count {
|
||||
margin-top: 4px;
|
||||
line-height: 11px;
|
||||
}
|
||||
.NB-feedlist-hide-read-feeds .NB-feedlist .feed {
|
||||
display: none;
|
||||
}
|
||||
|
@ -923,7 +907,7 @@ a img {
|
|||
float: right;
|
||||
font-weight: bold;
|
||||
color: #FFF;
|
||||
padding: 2px 1px 2px;
|
||||
padding: 3px 1px 1px;
|
||||
margin: 3px 1px 0;
|
||||
background-color: #8eb6e8;
|
||||
display: none;
|
||||
|
@ -932,16 +916,83 @@ a img {
|
|||
/* border-top: 1px solid rgba(255, 255, 255, .4);*/
|
||||
border-bottom: 1px solid rgba(0, 0, 0, .1);
|
||||
}
|
||||
.NB-theme-feed-size-l .unread_count {
|
||||
.NB-theme-feed-size-xs .NB-feedlist .unread_count {
|
||||
margin-top: 2px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.NB-theme-feed-size-s .NB-feedlist .unread_count {
|
||||
margin-top: 2px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.NB-theme-feed-size-l .NB-feedlist .unread_count {
|
||||
margin-top: 3px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
.NB-theme-feed-size-xl .unread_count {
|
||||
.NB-theme-feed-size-xl .NB-feedlist .unread_count {
|
||||
margin-top: 3px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.NB-feeds-header .unread_count {
|
||||
line-height: 11px;
|
||||
}
|
||||
.NB-theme-feed-size-xs .NB-feeds-header .unread_count {
|
||||
margin-top: 4px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.NB-theme-feed-size-s .NB-feeds-header .unread_count {
|
||||
margin-top: 4px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.NB-theme-feed-size-m .NB-feeds-header .unread_count {
|
||||
margin-top: 4px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.NB-theme-feed-size-l .NB-feeds-header .unread_count {
|
||||
margin-top: 3px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
.NB-theme-feed-size-xl .NB-feeds-header .unread_count {
|
||||
margin-top: 3px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.folder_title .unread_count {
|
||||
line-height: 15px;
|
||||
}
|
||||
.NB-theme-feed-size-xs .folder_title .unread_count {
|
||||
margin-top: -3px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.NB-theme-feed-size-s .folder_title .unread_count {
|
||||
margin-top: -3px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.NB-theme-feed-size-m .folder_title .unread_count {
|
||||
margin-top: -2px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.NB-theme-feed-size-l .folder_title .unread_count {
|
||||
margin-top: -2px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
.NB-theme-feed-size-xl .folder_title .unread_count {
|
||||
margin-top: -2px;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.unread_count_starred {
|
||||
background-color: #506B9A;
|
||||
text-shadow: 0 1px 0 rgba(0, 0, 0, .3);
|
||||
|
@ -3472,7 +3523,7 @@ body {
|
|||
margin-bottom: 32px;
|
||||
text-align: center;
|
||||
width: 194px;
|
||||
line-height: 1em;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.NB-narrow-content .NB-feed-story-sideoptions-container {
|
||||
|
@ -5314,6 +5365,10 @@ form.opml_import_form input {
|
|||
background-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.NB-add-form .NB-add-folder-icon:hover {
|
||||
background: transparent url('/media/embed/icons/circular/g_icn_folder_add_dark.png') no-repeat 0 0;
|
||||
background-size: 16px;
|
||||
}
|
||||
|
||||
.NB-add-form .NB-loading {
|
||||
float: right;
|
||||
|
@ -6855,6 +6910,9 @@ form.opml_import_form input {
|
|||
padding-top: 3px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
.NB-menu-manage .NB-menu-manage-feed-move-confirm {
|
||||
padding: 0;
|
||||
}
|
||||
.NB-menu-manage .NB-menu-manage-confirm .NB-menu-manage-confirm-position {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
|
@ -6928,6 +6986,75 @@ form.opml_import_form input {
|
|||
}
|
||||
.NB-menu-manage .NB-menu-manage-confirm .NB-add-folders {
|
||||
float: left;
|
||||
max-height: 84px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.NB-menu-manage .NB-menu-manage-feed-move-save {
|
||||
display: none;
|
||||
}
|
||||
.NB-menu-manage .NB-menu-manage-feed-move-save {
|
||||
float: right;
|
||||
display: none;
|
||||
}
|
||||
.NB-menu-manage .NB-menu-manage-confirm .NB-change-folders {
|
||||
width: 100%;
|
||||
height: 84px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.NB-menu-manage .NB-change-folders .NB-folders {
|
||||
padding: 4px 0;
|
||||
}
|
||||
.NB-menu-manage .NB-change-folders .NB-folder-option {
|
||||
overflow: hidden;
|
||||
clear: both;
|
||||
cursor: pointer;
|
||||
}
|
||||
.NB-menu-manage .NB-change-folders .NB-folder-option:hover {
|
||||
background-color: rgba(0, 4, 0, .1);
|
||||
}
|
||||
|
||||
.NB-menu-manage .NB-change-folders .NB-folder-option .NB-icon,
|
||||
.NB-menu-manage .NB-change-folders .NB-add-folder-form .NB-icon {
|
||||
background: transparent url('/media/embed/icons/circular/g_icn_folder.png') no-repeat 0 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-size: 16px;
|
||||
float: left;
|
||||
clear: left;
|
||||
margin: 1px 4px;
|
||||
}
|
||||
.NB-menu-manage .NB-change-folders .NB-folder-option.NB-folder-option-active .NB-icon {
|
||||
background: transparent url('/media/embed/icons/circular/g_icn_folder_rss.png') no-repeat 0 0;
|
||||
background-size: 16px;
|
||||
}
|
||||
.NB-menu-manage .NB-change-folders .NB-folder-option:hover .NB-icon-add {
|
||||
background: transparent url('/media/embed/icons/circular/g_icn_folder_add.png') no-repeat 0 0;
|
||||
float: right;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 1px 4px 0;
|
||||
background-size: 16px;
|
||||
}
|
||||
.NB-menu-manage .NB-change-folders .NB-folder-option:hover .NB-icon-add:hover {
|
||||
background: transparent url('/media/embed/icons/circular/g_icn_folder_add_dark.png') no-repeat 0 0;
|
||||
background-size: 16px;
|
||||
}
|
||||
|
||||
.NB-menu-manage .NB-change-folders .NB-folder-option-title {
|
||||
padding: 3px 0 2px;
|
||||
}
|
||||
.NB-menu-manage .NB-change-folders .NB-folder-option.NB-folder-option-active .NB-folder-option-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
.NB-menu-manage .NB-change-folders .NB-input {
|
||||
font-size: 11px;
|
||||
float: left;
|
||||
margin: 0;
|
||||
}
|
||||
.NB-menu-manage .NB-change-folders .NB-menu-manage-add-folder-save {
|
||||
margin: 0 4px 0 0;
|
||||
}
|
||||
|
||||
}
|
||||
.NB-menu-manage .NB-menu-manage-story-share-confirm .NB-sideoption-share {
|
||||
overflow: hidden;
|
||||
|
@ -6970,6 +7097,10 @@ form.opml_import_form input {
|
|||
.NB-menu-manage .NB-menu-manage-feedchooser .NB-menu-manage-image {
|
||||
background: transparent url('/media/embed/icons/silk/color_swatch.png') no-repeat 1px 2px;
|
||||
}
|
||||
.NB-menu-manage .NB-menu-manage-organizer .NB-menu-manage-image {
|
||||
background: transparent url('/media/embed/icons/circular/menu_icn_book.png') no-repeat 0 0;
|
||||
background-size: 18px;
|
||||
}
|
||||
.NB-menu-manage .NB-menu-manage-premium .NB-menu-manage-image {
|
||||
background: transparent url('/media/embed/icons/circular/g_icn_greensun.png') no-repeat 0 0px;
|
||||
background-size: 18px;
|
||||
|
@ -8689,7 +8820,7 @@ form.opml_import_form input {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.NB-modal-feedchooser .NB-feedchooser .feed_counts {
|
||||
.NB-modal-feedchooser .NB-feedchooser .feed .feed_counts {
|
||||
display: block;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
@ -8697,30 +8828,36 @@ form.opml_import_form input {
|
|||
.NB-modal-feedchooser #NB-feedchooser-feeds .feed {
|
||||
display: block;
|
||||
}
|
||||
.NB-modal-feedchooser .feed.NB-feedchooser-approve .feed_title {
|
||||
.NB-modal-feedchooser .NB-feed-organizer-sort {
|
||||
display: none;
|
||||
}
|
||||
.NB-modal-feedchooser .feed.NB-highlighted .feed_title {
|
||||
font-weight: bold;
|
||||
}
|
||||
.NB-modal-feedchooser .feed.NB-feedchooser-decline {
|
||||
.NB-modal-feedchooser .feed {
|
||||
font-weight: normal;
|
||||
}
|
||||
.NB-modal-feedchooser .feed.NB-feedchooser-decline .feed_favicon {
|
||||
.NB-modal-feedchooser .feed:not(.NB-highlighted) .feed_favicon {
|
||||
opacity: .3;
|
||||
}
|
||||
.NB-modal-feedchooser .feed.NB-feedchooser-decline .feed_title {
|
||||
.NB-modal-feedchooser .feed:not(.NB-highlighted) .feed_title {
|
||||
color: #808080;
|
||||
}
|
||||
.NB-modal-feedchooser .feed .feed_counts .unread_count_negative,
|
||||
.NB-modal-feedchooser .feed .feed_counts .unread_count_positive {
|
||||
display: none;
|
||||
}
|
||||
.NB-modal-feedchooser .feed.NB-feedchooser-approve .feed_counts,
|
||||
.NB-modal-feedchooser .feed.NB-feedchooser-approve .feed_counts .unread_count_positive {
|
||||
.NB-modal-feedchooser .feed.NB-highlighted .feed_counts,
|
||||
.NB-modal-feedchooser .feed.NB-highlighted .feed_counts .unread_count_positive {
|
||||
display: block;
|
||||
}
|
||||
.NB-modal-feedchooser .feed.NB-feedchooser-decline .feed_counts,
|
||||
.NB-modal-feedchooser .feed.NB-feedchooser-decline .feed_counts .unread_count_negative {
|
||||
.NB-modal-feedchooser .feed .feed_counts,
|
||||
.NB-modal-feedchooser .feed .feed_counts .unread_count_negative {
|
||||
display: block;
|
||||
}
|
||||
.NB-modal-feedchooser .feed.NB-highlighted .feed_counts .unread_count_negative {
|
||||
display: none;
|
||||
}
|
||||
.NB-modal-feedchooser .NB-feedlist .folder_title {
|
||||
cursor: default;
|
||||
}
|
||||
|
@ -10926,6 +11063,213 @@ form.opml_import_form input {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ============= */
|
||||
/* = Organizer = */
|
||||
/* ============= */
|
||||
|
||||
.NB-modal-organizer .NB-modal-title .NB-icon {
|
||||
background: transparent url('/media/embed/icons/circular/g_modal_book.png');
|
||||
background-size: 28px;
|
||||
}
|
||||
.NB-modal-organizer .NB-feedlist {
|
||||
width: 70%;
|
||||
max-height: 500px;
|
||||
border: 1px solid rgba(0, 0, 0, .2);
|
||||
margin: 4px 24px 0 0;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.NB-modal-organizer .NB-feedlist .feed_title {
|
||||
padding-right: 186px;
|
||||
}
|
||||
.NB-modal-organizer .NB-feedlist .feed {
|
||||
display: block;
|
||||
}
|
||||
.NB-modal-organizer .NB-feedlist .folder .folder_title .NB-feedlist-collapse-icon,
|
||||
.NB-modal-organizer .NB-feedlist .folder .folder_title .feed_counts_floater {
|
||||
display: none;
|
||||
}
|
||||
.NB-modal-organizer .NB-feedlist .feed.NB-feed-exception .feed_counts {
|
||||
display: block;
|
||||
}
|
||||
.NB-modal-organizer .feed.NB-highlighted .NB-feed-exception-icon {
|
||||
display: none;
|
||||
}
|
||||
.NB-modal-organizer .feed.selected {
|
||||
background: none;
|
||||
border-top: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
.NB-modal-organizer .feed {
|
||||
font-weight: normal;
|
||||
}
|
||||
.NB-modal-organizer .NB-highlighted {
|
||||
font-weight: bold;
|
||||
}
|
||||
.NB-modal-organizer .unread_count.unread_count_positive.unread_count_full {
|
||||
display: none;
|
||||
}
|
||||
.NB-modal-organizer .NB-highlighted .unread_count.unread_count_positive {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 13px;
|
||||
text-indent: -9999px;
|
||||
background-image: url('/media/embed/icons/circular/checkmark_white.png');
|
||||
background-size: 16px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.NB-modal-organizer .NB-feed-organizer-sort {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 42px;
|
||||
width: 124px;
|
||||
top: 4px;
|
||||
}
|
||||
.NB-modal-organizer .NB-sort-alphabetical .NB-feed-organizer-opens {
|
||||
display: block;
|
||||
}
|
||||
.NB-modal-organizer .NB-sort-subscribers .NB-feed-organizer-subscribers {
|
||||
display: block;
|
||||
}
|
||||
.NB-modal-organizer .NB-sort-recency .NB-feed-organizer-laststory {
|
||||
display: block;
|
||||
}
|
||||
.NB-modal-organizer .NB-sort-frequency .NB-feed-organizer-monthlycount {
|
||||
display: block;
|
||||
}
|
||||
.NB-modal-organizer .NB-sort-mostused .NB-feed-organizer-opens {
|
||||
display: block;
|
||||
}
|
||||
.NB-modal-organizer .NB-feed-organizer-sort.NB-active {
|
||||
display: block;
|
||||
}
|
||||
.NB-modal-organizer .NB-organizer-actionbar {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
margin: 12px 0 0 0;
|
||||
width: 70%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.NB-modal-organizer .NB-organizer-sorts {
|
||||
float: right;
|
||||
}
|
||||
.NB-modal-organizer .NB-organizer-selects {
|
||||
float: left;
|
||||
}
|
||||
.NB-modal-organizer .NB-organizer-action-title {
|
||||
float: left;
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.NB-modal-organizer .NB-organizer-action {
|
||||
float: left;
|
||||
margin-left: 6px;
|
||||
cursor: pointer;
|
||||
color: #405BA8;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.NB-modal-organizer .NB-organizer-action:hover {
|
||||
color: #A85B40;
|
||||
}
|
||||
.NB-modal-organizer .NB-organizer-action.NB-active,
|
||||
.NB-modal-organizer .NB-organizer-action.NB-active:hover {
|
||||
color: #000;
|
||||
padding-right: 12px;
|
||||
background: transparent url('/media/embed/icons/circular/g_icn_arrow_down.png') no-repeat right 3px;
|
||||
background-size: 8px;
|
||||
}
|
||||
.NB-modal-organizer .NB-sort-inverse .NB-organizer-action.NB-active {
|
||||
background: transparent url('/media/embed/icons/circular/g_icn_arrow_up.png') no-repeat right 4px;
|
||||
background-size: 8px;
|
||||
}
|
||||
.NB-modal-organizer .NB-organizer-selects .NB-organizer-action {
|
||||
float: left;
|
||||
margin-left: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.NB-modal-organizer .NB-organizer-selects .NB-organizer-action-title {
|
||||
margin-left: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.NB-modal-organizer .NB-organizer-sidebar {
|
||||
float: right;
|
||||
width: 27%;
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
.NB-modal-organizer .NB-organizer-sidebar-title {
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
font-weight: bold;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.NB-modal-organizer .NB-organizer-sidebar-hierarchy .NB-organizer-sidebar-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
.NB-modal-organizer .NB-organizer-sidebar-container {
|
||||
margin: 4px 0 0;
|
||||
border: 1px solid rgba(0, 0, 0, .2);
|
||||
background-color: #F7F8F5;
|
||||
padding: 6px;
|
||||
}
|
||||
.NB-modal-organizer .segmented-control {
|
||||
margin: 0;
|
||||
line-height: 18px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
}
|
||||
.NB-modal-organizer .segmented-control li {
|
||||
padding: 2px 12px 0;
|
||||
font-size: 11px;
|
||||
width: 50%;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.NB-modal-organizer .NB-modal-submit-button {
|
||||
text-align: center;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
.NB-modal-organizer .NB-action-delete {
|
||||
margin-top: 0;
|
||||
}
|
||||
.NB-modal-organizer .NB-folders {
|
||||
width: 164px;
|
||||
}
|
||||
.NB-modal-organizer .NB-icon-add {
|
||||
background: transparent url('/media/embed/icons/circular/g_icn_folder_add.png') no-repeat 0 0;
|
||||
float: right;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 4px 4px 0;
|
||||
cursor: pointer;
|
||||
background-size: 16px;
|
||||
}
|
||||
.NB-modal-organizer .NB-icon-add:hover {
|
||||
background: transparent url('/media/embed/icons/circular/g_icn_folder_add_dark.png') no-repeat 0 0;
|
||||
background-size: 16px;
|
||||
}
|
||||
.NB-modal-organizer .NB-icon-subfolder {
|
||||
background: transparent url('/media/embed/icons/circular/g_icn_arrow_right.png') no-repeat 0 0;
|
||||
float: left;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin: 12px 6px 0 4px;
|
||||
background-size: 8px;
|
||||
}
|
||||
.NB-modal-organizer .NB-add-folder-input {
|
||||
font-size: 11px;
|
||||
width: 140px;
|
||||
margin: 6px 0 2px;
|
||||
}
|
||||
|
||||
/* ================= */
|
||||
/* = Feed Selector = */
|
||||
/* ================= */
|
||||
|
@ -11464,6 +11808,15 @@ form.opml_import_form input {
|
|||
.NB-module-search .NB-module-search-results {
|
||||
padding: 12px;
|
||||
}
|
||||
.NB-module-search .NB-friends-search-badges-empty {
|
||||
clear: both;
|
||||
font-size: 14px;
|
||||
font-weight: 100;
|
||||
}
|
||||
.NB-module-search .NB-friends-search-badges-empty .NB-raquo {
|
||||
float: left;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.NB-module-search-input input.NB-active {
|
||||
background: white url('/media/embed/reader/recycle_spinner.gif') no-repeat right 4px;
|
||||
|
|
BIN
media/img/icons/circular/book.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
media/img/icons/circular/checkmark_white.png
Normal file
After Width: | Height: | Size: 383 B |
BIN
media/img/icons/circular/g_icn_folder_add_dark.png
Normal file
After Width: | Height: | Size: 838 B |
BIN
media/img/icons/circular/g_modal_book.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
media/img/icons/circular/menu_icn_book.png
Normal file
After Width: | Height: | Size: 938 B |
|
@ -502,7 +502,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
|
|||
feed_address: this.feeds.get(feed_id).get('feed_address'),
|
||||
order: this.view_setting(feed_id, 'order'),
|
||||
read_filter: this.view_setting(feed_id, 'read_filter'),
|
||||
query: NEWSBLUR.reader.flags.search
|
||||
query: NEWSBLUR.reader.flags.search,
|
||||
include_hidden: true
|
||||
}, pre_callback,
|
||||
error_callback,
|
||||
{
|
||||
|
@ -662,7 +663,8 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
|
|||
page: page,
|
||||
order: this.view_setting(feed_id, 'order'),
|
||||
read_filter: this.view_setting(feed_id, 'read_filter'),
|
||||
query: NEWSBLUR.reader.flags.search
|
||||
query: NEWSBLUR.reader.flags.search,
|
||||
include_hidden: true
|
||||
}, pre_callback, error_callback, {
|
||||
'ajax_group': (page ? 'feed_page' : 'feed'),
|
||||
'request_type': 'GET'
|
||||
|
@ -1013,6 +1015,20 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
|
|||
}
|
||||
},
|
||||
|
||||
delete_feeds_by_folder: function(feeds_by_folder, callback, error_callback) {
|
||||
var pre_callback = _.bind(function(data) {
|
||||
_.each(feeds_by_folder, _.bind(function(feed_in_folder) {
|
||||
this.feeds.remove(feed_in_folder[0]);
|
||||
}, this));
|
||||
this.folders.reset(_.compact(data.folders), {parse: true});
|
||||
return callback();
|
||||
}, this);
|
||||
|
||||
this.make_request('/reader/delete_feeds_by_folder', {
|
||||
'feeds_by_folder': $.toJSON(feeds_by_folder)
|
||||
}, pre_callback, error_callback);
|
||||
},
|
||||
|
||||
delete_feed_by_url: function(url, in_folder, callback) {
|
||||
this.make_request('/reader/delete_feed_by_url/', {
|
||||
'url': url,
|
||||
|
@ -1023,12 +1039,19 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
|
|||
},
|
||||
|
||||
delete_folder: function(folder_name, in_folder, feeds, callback) {
|
||||
var self = this;
|
||||
var pre_callback = function(data) {
|
||||
self.folders.reset(_.compact(data.folders), {parse: true});
|
||||
self.feeds.trigger('reset');
|
||||
|
||||
callback(data);
|
||||
};
|
||||
if (NEWSBLUR.Globals.is_authenticated) {
|
||||
this.make_request('/reader/delete_folder', {
|
||||
'folder_name': folder_name,
|
||||
'in_folder': in_folder,
|
||||
'feed_id': feeds
|
||||
}, callback, null);
|
||||
}, pre_callback, null);
|
||||
} else {
|
||||
if ($.isFunction(callback)) callback();
|
||||
}
|
||||
|
@ -1090,6 +1113,19 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
|
|||
}, pre_callback);
|
||||
},
|
||||
|
||||
move_feed_to_folders: function(feed_id, in_folders, to_folders, callback) {
|
||||
var pre_callback = _.bind(function(data) {
|
||||
this.folders.reset(_.compact(data.folders), {parse: true});
|
||||
return callback();
|
||||
}, this);
|
||||
|
||||
this.make_request('/reader/move_feed_to_folders', {
|
||||
'feed_id': feed_id,
|
||||
'in_folders': in_folders,
|
||||
'to_folders': to_folders
|
||||
}, pre_callback);
|
||||
},
|
||||
|
||||
move_folder_to_folder: function(folder_name, in_folder, to_folder, callback) {
|
||||
var pre_callback = _.bind(function(data) {
|
||||
this.folders.reset(_.compact(data.folders), {parse: true});
|
||||
|
@ -1103,6 +1139,19 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
|
|||
}, pre_callback);
|
||||
},
|
||||
|
||||
move_feeds_by_folder: function(feeds_by_folder, to_folder, new_folder, callback, error_callback) {
|
||||
var pre_callback = _.bind(function(data) {
|
||||
this.folders.reset(_.compact(data.folders), {parse: true});
|
||||
return callback();
|
||||
}, this);
|
||||
|
||||
this.make_request('/reader/move_feeds_by_folder_to_folder', {
|
||||
'feeds_by_folder': $.toJSON(feeds_by_folder),
|
||||
'to_folder': to_folder,
|
||||
'new_folder': new_folder
|
||||
}, pre_callback, error_callback);
|
||||
},
|
||||
|
||||
preference: function(preference, value, callback) {
|
||||
if (typeof value == 'undefined') {
|
||||
var pref = NEWSBLUR.Preferences[preference];
|
||||
|
|
|
@ -78,6 +78,29 @@ NEWSBLUR.Models.Feed = Backbone.Model.extend({
|
|||
return true;
|
||||
},
|
||||
|
||||
move_to_folders: function(to_folders, options) {
|
||||
options = options || {};
|
||||
var view = options.view || this.get_view();
|
||||
var in_folders = this.in_folders();
|
||||
|
||||
if (_.isEqual(in_folders, to_folders)) return false;
|
||||
|
||||
NEWSBLUR.reader.flags['reloading_feeds'] = true;
|
||||
NEWSBLUR.assets.move_feed_to_folders(this.id, in_folders, to_folders, function() {
|
||||
NEWSBLUR.reader.flags['reloading_feeds'] = false;
|
||||
_.delay(function() {
|
||||
NEWSBLUR.reader.$s.$feed_list.css('opacity', 1).animate({'opacity': 0}, {
|
||||
'duration': 100,
|
||||
'complete': function() {
|
||||
NEWSBLUR.app.feed_list.make_feeds();
|
||||
}
|
||||
});
|
||||
}, 250);
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
parent_folder_names: function() {
|
||||
var names = _.compact(_.flatten(_.map(this.folders, function(folder) {
|
||||
return folder.parent_folder_names();
|
||||
|
@ -86,6 +109,12 @@ NEWSBLUR.Models.Feed = Backbone.Model.extend({
|
|||
return names;
|
||||
},
|
||||
|
||||
in_folders: function() {
|
||||
var in_folders = _.pluck(_.pluck(this.folders, 'options'), 'title');
|
||||
|
||||
return in_folders;
|
||||
},
|
||||
|
||||
rename: function(new_title) {
|
||||
this.set('feed_title', new_title);
|
||||
NEWSBLUR.assets.rename_feed(this.id, new_title);
|
||||
|
@ -167,6 +196,65 @@ NEWSBLUR.Models.Feed = Backbone.Model.extend({
|
|||
} else if (unread_view > 0) {
|
||||
return !!(this.get('ps'));
|
||||
}
|
||||
},
|
||||
|
||||
relative_last_story_date: function() {
|
||||
var diff = this.get('last_story_seconds_ago');
|
||||
var lasthour = 60*60;
|
||||
var lastday = 24*60*60;
|
||||
|
||||
if (diff > 1000*60*60*24*365*20 || diff <= 0) {
|
||||
return "Never";
|
||||
} else if (diff < lasthour) {
|
||||
return Inflector.pluralize("minute", Math.floor(diff/60), true) + " ago";
|
||||
} else if (diff < lastday) {
|
||||
return Inflector.pluralize("hour", Math.floor(diff/60/60), true) + " ago";
|
||||
} else {
|
||||
return Inflector.pluralize("day", Math.floor(diff/60/60/24), true) + " ago";
|
||||
}
|
||||
},
|
||||
|
||||
highlighted_in_folder: function(folder_title) {
|
||||
return !!(this.get('highlighted') &&
|
||||
this.get('highlighted_in_folders') &&
|
||||
_.contains(this.get('highlighted_in_folders'), folder_title));
|
||||
},
|
||||
|
||||
highlight_in_folder: function(folder_title, on, off, options) {
|
||||
options = options || {};
|
||||
|
||||
if (!this.get('highlighted_in_folders')) {
|
||||
this.set('highlighted_in_folders', [], {silent: true});
|
||||
}
|
||||
|
||||
if (!off && (on || !_.contains(this.get('highlighted_in_folders'), folder_title))) {
|
||||
this.set('highlighted_in_folders',
|
||||
this.get('highlighted_in_folders').concat(folder_title), {silent: true});
|
||||
} else {
|
||||
this.set('highlighted_in_folders',
|
||||
_.without(this.get('highlighted_in_folders'), folder_title), {silent: true});
|
||||
}
|
||||
this.set('highlighted', !!this.get('highlighted_in_folders').length, {silent: true});
|
||||
|
||||
if (!options.silent) this.trigger('change:highlighted');
|
||||
},
|
||||
|
||||
highlight_in_all_folders: function(on, off, options) {
|
||||
options = options || {};
|
||||
|
||||
if (!this.get('highlighted_in_folders')) {
|
||||
this.set('highlighted_in_folders', [], {silent: true});
|
||||
}
|
||||
var folders = _.unique(this.in_folders()) || [];
|
||||
|
||||
if (!off && (on || !this.get('highlighted_in_folders').length)) {
|
||||
this.set('highlighted_in_folders', folders, {silent: true});
|
||||
} else {
|
||||
this.set('highlighted_in_folders', [], {silent: true});
|
||||
}
|
||||
this.set('highlighted', !!this.get('highlighted_in_folders').length, {silent: true});
|
||||
|
||||
if (!options.silent) this.trigger('change:highlighted');
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -52,11 +52,11 @@ NEWSBLUR.Models.FeedOrFolder = Backbone.Model.extend({
|
|||
return view;
|
||||
},
|
||||
|
||||
feed_ids_in_folder: function() {
|
||||
if (this.is_feed() && this.feed.get('active')) {
|
||||
feed_ids_in_folder: function(include_inactive) {
|
||||
if (this.is_feed() && (include_inactive || (!include_inactive && this.feed.get('active')))) {
|
||||
return this.feed.id;
|
||||
} else if (this.is_folder()) {
|
||||
return this.folders.feed_ids_in_folder();
|
||||
return this.folders.feed_ids_in_folder(include_inactive);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -138,7 +138,7 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({
|
|||
|
||||
initialize: function(models, options) {
|
||||
_.bindAll(this, 'propagate_feed_selected');
|
||||
this.options = options || {};
|
||||
this.options = _.extend({}, this.options, options);
|
||||
this.parent_folder = options && options.parent_folder;
|
||||
this.comparator = NEWSBLUR.Collections.Folders.comparator;
|
||||
this.bind('change:feed_selected', this.propagate_feed_selected);
|
||||
|
@ -215,9 +215,9 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({
|
|||
return names;
|
||||
},
|
||||
|
||||
feed_ids_in_folder: function() {
|
||||
feed_ids_in_folder: function(include_inactive) {
|
||||
return _.compact(_.flatten(this.map(function(item) {
|
||||
return item.feed_ids_in_folder();
|
||||
return item.feed_ids_in_folder(include_inactive);
|
||||
})));
|
||||
},
|
||||
|
||||
|
@ -322,8 +322,19 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({
|
|||
}, {
|
||||
|
||||
comparator: function(modelA, modelB) {
|
||||
var sort_order = NEWSBLUR.assets.preference('feed_order');
|
||||
|
||||
// toUpperCase for historical reasons
|
||||
var sort_order = NEWSBLUR.assets.preference('feed_order').toUpperCase();
|
||||
|
||||
if (NEWSBLUR.Collections.Folders.organizer_sortorder) {
|
||||
sort_order = NEWSBLUR.Collections.Folders.organizer_sortorder.toUpperCase();
|
||||
}
|
||||
var high = 1;
|
||||
var low = -1;
|
||||
if (NEWSBLUR.Collections.Folders.organizer_inversesort) {
|
||||
high = -1;
|
||||
low = 1;
|
||||
}
|
||||
|
||||
if (modelA.is_feed() != modelB.is_feed()) {
|
||||
// Feeds above folders
|
||||
return modelA.is_feed() ? -1 : 1;
|
||||
|
@ -341,11 +352,23 @@ NEWSBLUR.Collections.Folders = Backbone.Collection.extend({
|
|||
}
|
||||
|
||||
if (sort_order == 'ALPHABETICAL' || !sort_order) {
|
||||
return feedA.get('feed_title').toLowerCase() > feedB.get('feed_title').toLowerCase() ? 1 : -1;
|
||||
return feedA.get('feed_title').toLowerCase() > feedB.get('feed_title').toLowerCase() ? high : low;
|
||||
} else if (sort_order == 'MOSTUSED') {
|
||||
return feedA.get('feed_opens') < feedB.get('feed_opens') ? 1 :
|
||||
(feedA.get('feed_opens') > feedB.get('feed_opens') ? -1 :
|
||||
(feedA.get('feed_title').toLowerCase() > feedB.get('feed_title').toLowerCase() ? 1 : -1));
|
||||
return feedA.get('feed_opens') < feedB.get('feed_opens') ? high :
|
||||
(feedA.get('feed_opens') > feedB.get('feed_opens') ? low :
|
||||
(feedA.get('feed_title').toLowerCase() > feedB.get('feed_title').toLowerCase() ? high : low));
|
||||
} else if (sort_order == 'RECENCY') {
|
||||
return feedA.get('last_story_seconds_ago') < feedB.get('last_story_seconds_ago') ? high :
|
||||
(feedA.get('last_story_seconds_ago') > feedB.get('last_story_seconds_ago') ? low :
|
||||
(feedA.get('feed_title').toLowerCase() > feedB.get('feed_title').toLowerCase() ? high : low));
|
||||
} else if (sort_order == 'FREQUENCY') {
|
||||
return feedA.get('average_stories_per_month') < feedB.get('average_stories_per_month') ? high :
|
||||
(feedA.get('average_stories_per_month') > feedB.get('average_stories_per_month') ? low :
|
||||
(feedA.get('feed_title').toLowerCase() > feedB.get('feed_title').toLowerCase() ? high : low));
|
||||
} else if (sort_order == 'SUBSCRIBERS') {
|
||||
return feedA.get('num_subscribers') < feedB.get('num_subscribers') ? high :
|
||||
(feedA.get('num_subscribers') > feedB.get('num_subscribers') ? low :
|
||||
(feedA.get('feed_title').toLowerCase() > feedB.get('feed_title').toLowerCase() ? high : low));
|
||||
}
|
||||
}
|
||||
|
||||
|
|