Merge branch 'master' into shareext

* master: (53 commits)
  Increasing timeout on feedfinder, just in case (see https://blog.dbrgn.ch/).
  Handling multiple stripe ids.
  If no iOS receipt, still send proof to server.
  Don't upgrade premium users on stripe reimport.
  Handling missing customer.
  Fixing stripe import history.
  Fixing stripe import history.
  Fixing logging for mercury parser error.
  Adding images to shared twitter stories.
  Creating MSocialServices when necessary.
  Fixing truncate chars to do a proper unicode truncate.
  Handling APNS unicode encode errors.
  Fixing APNS notifications.
  Attempting to corral APNS push notification max sizes due to chinese unicode characters being 3 bytes.
  Handling missing user sub folders for IFTTT.
  Handling missing original page.
  Handling mercury text parsing error.
  Fixing issue with double saving of classifier.
  Android v9b1.
  remove cruft
  ...
This commit is contained in:
Samuel Clay 2018-08-25 14:15:21 -04:00
commit 4a9d604293
62 changed files with 1336 additions and 1082 deletions

View file

@ -15,7 +15,6 @@ from apps.social.models import MSocialSubscription
from utils import json_functions as json from utils import json_functions as json
from utils.user_functions import get_user from utils.user_functions import get_user
from utils.user_functions import ajax_login_required from utils.user_functions import ajax_login_required
from utils.view_functions import render_to
def index(requst): def index(requst):
pass pass
@ -84,10 +83,13 @@ def save_classifier(request):
try: try:
classifier = ClassifierCls.objects.get(**classifier_dict) classifier = ClassifierCls.objects.get(**classifier_dict)
except ClassifierCls.DoesNotExist: except ClassifierCls.DoesNotExist:
classifier_dict.update(dict(score=score)) classifier = None
classifier = ClassifierCls.objects.create(**classifier_dict) if not classifier:
except NotUniqueError: try:
continue classifier_dict.update(dict(score=score))
classifier = ClassifierCls.objects.create(**classifier_dict)
except NotUniqueError:
continue
if score == 0: if score == 0:
classifier.delete() classifier.delete()
elif classifier.score != score: elif classifier.score != score:

View file

@ -1,5 +1,4 @@
import urllib import urllib
import urlparse
import datetime import datetime
import lxml.html import lxml.html
import tweepy import tweepy
@ -284,7 +283,12 @@ def api_user_info(request):
@json.json_view @json.json_view
def api_feed_list(request, trigger_slug=None): def api_feed_list(request, trigger_slug=None):
user = request.user user = request.user
usf = UserSubscriptionFolders.objects.get(user=user) try:
usf = UserSubscriptionFolders.objects.get(user=user)
except UserSubscriptionFolders.DoesNotExist:
return {"errors": [{
'message': 'Could not find feeds for user.'
}]}
flat_folders = usf.flatten_folders() flat_folders = usf.flatten_folders()
titles = [dict(label=" - Folder: All Site Stories", value="all")] titles = [dict(label=" - Folder: All Site Stories", value="all")]
feeds = {} feeds = {}

View file

@ -2,61 +2,62 @@ import stripe, datetime, time
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
from optparse import make_option from optparse import make_option
from utils import log as logging from utils import log as logging
from apps.profile.models import Profile, PaymentHistory from apps.profile.models import Profile
class Command(BaseCommand): class Command(BaseCommand):
option_list = BaseCommand.option_list + ( option_list = BaseCommand.option_list + (
# make_option("-u", "--username", dest="username", nargs=1, help="Specify user id or username"), # make_option("-u", "--username", dest="username", nargs=1, help="Specify user id or username"),
# make_option("-e", "--email", dest="email", nargs=1, help="Specify email if it doesn't exist"), # make_option("-e", "--email", dest="email", nargs=1, help="Specify email if it doesn't exist"),
make_option("-d", "--days", dest="days", nargs=1, type='int', default=365, help="Number of days to go back"), make_option("-d", "--days", dest="days", nargs=1, type='int', default=365, help="Number of days to go back"),
make_option("-o", "--offset", dest="offset", nargs=1, type='int', default=0, help="Offset customer (in date DESC)"), make_option("-l", "--limit", dest="limit", nargs=1, type='int', default=100, help="Charges per batch"),
make_option("-s", "--start", dest="start", nargs=1, type='string', default=None, help="Offset customer_id (starting_after)"),
) )
def handle(self, *args, **options): def handle(self, *args, **options):
stripe.api_key = settings.STRIPE_SECRET stripe.api_key = settings.STRIPE_SECRET
week = (datetime.datetime.now() - datetime.timedelta(days=int(options.get('days', 365)))).strftime('%s') week = (datetime.datetime.now() - datetime.timedelta(days=int(options.get('days', 365)))).strftime('%s')
failed = [] failed = []
limit = 100 limit = options.get('limit')
offset = options.get('offset') starting_after = options.get('start')
i = 0
while True: while True:
logging.debug(" ---> At %s" % offset) logging.debug(" ---> At %s / %s" % (i, starting_after))
i += 1
try: try:
data = stripe.Charge.all(created={'gt': week}, count=limit, offset=offset) data = stripe.Charge.all(created={'gt': week}, count=limit, starting_after=starting_after)
except stripe.APIConnectionError: except stripe.APIConnectionError:
time.sleep(10) time.sleep(10)
continue continue
charges = data['data'] charges = data['data']
if not len(charges): if not len(charges):
logging.debug("At %s, finished" % offset) logging.debug("At %s (%s), finished" % (i, starting_after))
break break
offset += limit starting_after = charges[-1]["id"]
customers = [c['customer'] for c in charges if 'customer' in c] customers = [c['customer'] for c in charges if 'customer' in c]
for customer in customers: for customer in customers:
if not customer:
print " ***> No customer!"
continue
try: try:
profile = Profile.objects.get(stripe_id=customer) profile = Profile.objects.get(stripe_id=customer)
user = profile.user user = profile.user
except Profile.DoesNotExist: except Profile.DoesNotExist:
logging.debug(" ***> Couldn't find stripe_id=%s" % customer) logging.debug(" ***> Couldn't find stripe_id=%s" % customer)
failed.append(customer) failed.append(customer)
continue
except Profile.MultipleObjectsReturned:
logging.debug(" ***> Multiple stripe_id=%s" % customer)
failed.append(customer)
continue
try: try:
if not user.profile.is_premium: user.profile.setup_premium_history()
user.profile.activate_premium()
elif user.payments.all().count() != 1:
user.profile.setup_premium_history()
elif not user.profile.premium_expire:
user.profile.setup_premium_history()
elif user.profile.premium_expire > datetime.datetime.now() + datetime.timedelta(days=365):
user.profile.setup_premium_history()
else:
logging.debug(" ---> %s is fine" % user.username)
except stripe.APIConnectionError: except stripe.APIConnectionError:
logging.debug(" ***> Failed: %s" % user.username) logging.debug(" ***> Failed: %s" % user.username)
failed.append(username) failed.append(user.username)
time.sleep(2) time.sleep(2)
continue continue

View file

@ -348,20 +348,23 @@ def stripe_form(request):
user.profile.premium_expire > datetime.datetime.now()) user.profile.premium_expire > datetime.datetime.now())
# Are they changing their existing card? # Are they changing their existing card?
if user.profile.stripe_id and current_premium: if user.profile.stripe_id:
customer = stripe.Customer.retrieve(user.profile.stripe_id) customer = stripe.Customer.retrieve(user.profile.stripe_id)
try: try:
card = customer.cards.create(card=zebra_form.cleaned_data['stripe_token']) card = customer.sources.create(source=zebra_form.cleaned_data['stripe_token'])
except stripe.CardError: except stripe.CardError:
error = "This card was declined." error = "This card was declined."
else: else:
customer.default_card = card.id customer.default_card = card.id
customer.save() customer.save()
user.profile.strip_4_digits = zebra_form.cleaned_data['last_4_digits']
user.profile.save()
user.profile.activate_premium() # TODO: Remove, because webhooks are slow
success_updating = True success_updating = True
else: else:
try: try:
customer = stripe.Customer.create(**{ customer = stripe.Customer.create(**{
'card': zebra_form.cleaned_data['stripe_token'], 'source': zebra_form.cleaned_data['stripe_token'],
'plan': zebra_form.cleaned_data['plan'], 'plan': zebra_form.cleaned_data['plan'],
'email': user.email, 'email': user.email,
'description': user.username, 'description': user.username,
@ -375,19 +378,28 @@ def stripe_form(request):
user.profile.activate_premium() # TODO: Remove, because webhooks are slow user.profile.activate_premium() # TODO: Remove, because webhooks are slow
success_updating = True success_updating = True
if success_updating and customer and customer.subscriptions.count == 0: # Check subscription to ensure latest plan, otherwise cancel it and subscribe
billing_cycle_anchor = "now" if success_updating and customer and customer.subscriptions.total_count == 1:
if current_premium: subscription = customer.subscriptions.data[0]
billing_cycle_anchor = user.profile.premium_expire.strftime('%s') if subscription['plan']['id'] != "newsblur-premium-36":
stripe.Subscription.create( for sub in customer.subscriptions:
sub.delete()
customer = stripe.Customer.retrieve(user.profile.stripe_id)
if success_updating and customer and customer.subscriptions.total_count == 0:
params = dict(
customer=customer.id, customer=customer.id,
billing_cycle_anchor=billing_cycle_anchor,
items=[ items=[
{ {
"plan": "newsblur-premium-36", "plan": "newsblur-premium-36",
}, },
] ])
) premium_expire = user.profile.premium_expire
if current_premium and premium_expire:
if premium_expire < (datetime.datetime.now() + datetime.timedelta(days=365)):
params['billing_cycle_anchor'] = premium_expire.strftime('%s')
params['trial_end'] = premium_expire.strftime('%s')
stripe.Subscription.create(**params)
else: else:
zebra_form = StripePlusPaymentForm(email=user.email, plan=plan) zebra_form = StripePlusPaymentForm(email=user.email, plan=plan)

View file

@ -1344,7 +1344,7 @@ def load_river_stories__redis(request):
starred_stories = MStarredStory.objects( starred_stories = MStarredStory.objects(
user_id=user.pk, user_id=user.pk,
story_hash__in=story_hashes story_hash__in=story_hashes
).only('story_hash', 'starred_date') ).only('story_hash', 'starred_date', 'user_tags')
starred_stories = dict([(story.story_hash, dict(starred_date=story.starred_date, starred_stories = dict([(story.story_hash, dict(starred_date=story.starred_date,
user_tags=story.user_tags)) user_tags=story.user_tags))
for story in starred_stories]) for story in starred_stories])

View file

@ -446,7 +446,7 @@ class Feed(models.Model):
return feed return feed
@timelimit(5) @timelimit(10)
def _feedfinder(url): def _feedfinder(url):
found_feed_urls = feedfinder.find_feeds(url) found_feed_urls = feedfinder.find_feeds(url)
return found_feed_urls return found_feed_urls
@ -1136,7 +1136,6 @@ class Feed(models.Model):
if getattr(settings, 'TEST_DEBUG', False): if getattr(settings, 'TEST_DEBUG', False):
print " ---> Testing feed fetch: %s" % self.log_title print " ---> Testing feed fetch: %s" % self.log_title
options['force'] = False
# options['force_fp'] = True # No, why would this be needed? # options['force_fp'] = True # No, why would this be needed?
original_feed_address = self.feed_address original_feed_address = self.feed_address
original_feed_link = self.feed_link original_feed_link = self.feed_link
@ -2057,12 +2056,12 @@ class Feed(models.Model):
# SpD = 0 Subs > 1: t = 60 * 3 # 30158 * 1440/(60*3) = 241264 # SpD = 0 Subs > 1: t = 60 * 3 # 30158 * 1440/(60*3) = 241264
# SpD = 0 Subs = 1: t = 60 * 24 # 514131 * 1440/(60*24) = 514131 # SpD = 0 Subs = 1: t = 60 * 24 # 514131 * 1440/(60*24) = 514131
if spd >= 1: if spd >= 1:
if subs > 10: if subs >= 10:
total = 6 total = 6
elif subs > 1: elif subs > 1:
total = 15 total = 15
else: else:
total = 60 total = 45
elif spd > 0: elif spd > 0:
if subs > 1: if subs > 1:
total = 60 - (spd * 60) total = 60 - (spd * 60)
@ -2099,9 +2098,9 @@ class Feed(models.Model):
if len(fetch_history['push_history']): if len(fetch_history['push_history']):
total = total * 12 total = total * 12
# 12 hour max for premiums, 48 hour max for free # 6 hour max for premiums, 48 hour max for free
if subs >= 1: if subs >= 1:
total = min(total, 60*12*1) total = min(total, 60*6*1)
else: else:
total = min(total, 60*24*2) total = min(total, 60*24*2)

View file

@ -1,6 +1,7 @@
import requests import requests
import urllib3 import urllib3
import zlib import zlib
from simplejson.decoder import JSONDecodeError
from requests.packages.urllib3.exceptions import LocationParseError from requests.packages.urllib3.exceptions import LocationParseError
from socket import error as SocketError from socket import error as SocketError
from mongoengine.queryset import NotUniqueError from mongoengine.queryset import NotUniqueError
@ -69,9 +70,12 @@ class TextImporter:
if not resp: if not resp:
return return
doc = resp.json() try:
if doc.get('error', False): doc = resp.json()
logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: %s" % doc.get('messages', "[unknown merucry error]")) except JSONDecodeError:
doc = None
if not doc or doc.get('error', False):
logging.user(self.request, "~SN~FRFailed~FY to fetch ~FGoriginal text~FY: %s" % (doc and doc.get('messages', None) or "[unknown mercury error]"))
return return
text = doc['content'] text = doc['content']

View file

@ -526,7 +526,8 @@ def original_story(request):
if not story: if not story:
logging.user(request, "~FYFetching ~FGoriginal~FY story page: ~FRstory not found") logging.user(request, "~FYFetching ~FGoriginal~FY story page: ~FRstory not found")
return {'code': -1, 'message': 'Story not found.', 'original_page': None, 'failed': True} # return {'code': -1, 'message': 'Story not found.', 'original_page': None, 'failed': True}
raise Http404
original_page = story.fetch_original_page(force=force, request=request, debug=debug) original_page = story.fetch_original_page(force=force, request=request, debug=debug)

View file

@ -1,3 +1,5 @@
import os
import urlparse
import datetime import datetime
import time import time
import zlib import zlib
@ -2329,6 +2331,7 @@ class MSharedStory(mongo.DynamicDocument):
reverse=True) reverse=True)
self.image_sizes = image_sizes self.image_sizes = image_sizes
self.image_count = len(image_sizes) self.image_count = len(image_sizes)
self.image_urls = image_sources
self.save() self.save()
logging.debug(" ---> ~SN~FGFetched image sizes on shared story: ~SB%s/%s images" % logging.debug(" ---> ~SN~FGFetched image sizes on shared story: ~SB%s/%s images" %
@ -2788,14 +2791,38 @@ class MSocialServices(mongo.Document):
try: try:
api = self.twitter_api() api = self.twitter_api()
api.update_status(status=message) filename = self.fetch_image_file_for_twitter(shared_story)
if filename:
api.update_with_media(filename, status=message)
os.remove(filename)
else:
api.update_status(status=message)
except tweepy.TweepError, e: except tweepy.TweepError, e:
user = User.objects.get(pk=self.user_id) user = User.objects.get(pk=self.user_id)
logging.user(user, "~FRTwitter error: ~SB%s" % e) logging.user(user, "~FRTwitter error: ~SB%s" % e)
return return
return True return True
def fetch_image_file_for_twitter(self, shared_story):
if not shared_story.image_urls: return
user = User.objects.get(pk=self.user_id)
logging.user(user, "~FCFetching image for twitter: ~SB%s" % shared_story.image_urls[0])
url = shared_story.image_urls[0]
image_filename = os.path.basename(urlparse.urlparse(url).path)
req = requests.get(url, stream=True)
filename = "/tmp/%s-%s" % (shared_story.story_hash, image_filename)
if req.status_code == 200:
f = open(filename, "wb")
for chunk in req:
f.write(chunk)
f.close()
return filename
def post_to_facebook(self, shared_story): def post_to_facebook(self, shared_story):
message = shared_story.generate_post_to_service_message(include_url=False) message = shared_story.generate_post_to_service_message(include_url=False)
shared_story.calculate_image_sizes() shared_story.calculate_image_sizes()

View file

@ -987,7 +987,7 @@ def save_user_profile(request):
profile.private = is_true(data.get('private', False)) profile.private = is_true(data.get('private', False))
profile.save() profile.save()
social_services = MSocialServices.objects.get(user_id=request.user.pk) social_services = MSocialServices.get_user(user_id=request.user.pk)
profile = social_services.set_photo(data['photo_service']) profile = social_services.set_photo(data['photo_service'])
logging.user(request, "~BB~FRSaving social profile") logging.user(request, "~BB~FRSaving social profile")

View file

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.newsblur" package="com.newsblur"
android:versionCode="159" android:versionCode="160"
android:versionName="8.0.0" > android:versionName="9.0b1" >
<uses-sdk <uses-sdk
android:minSdkVersion="21" android:minSdkVersion="21"
android:targetSdkVersion="24" /> android:targetSdkVersion="26" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -137,7 +137,9 @@
<activity <activity
android:name=".activity.SocialFeedReading"/> android:name=".activity.SocialFeedReading"/>
<service android:name=".service.NBSyncService" /> <service
android:name=".service.NBSyncService"
android:permission="android.permission.BIND_JOB_SERVICE" />
<receiver android:name=".service.BootReceiver"> <receiver android:name=".service.BootReceiver">
<intent-filter> <intent-filter>
@ -145,8 +147,6 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".service.ServiceScheduleReceiver" />
<receiver android:name=".util.NotifyDismissReceiver" android:exported="false" /> <receiver android:name=".util.NotifyDismissReceiver" android:exported="false" />
<receiver android:name=".util.NotifySaveReceiver" android:exported="false" /> <receiver android:name=".util.NotifySaveReceiver" android:exported="false" />
<receiver android:name=".util.NotifyMarkreadReceiver" android:exported="false" /> <receiver android:name=".util.NotifyMarkreadReceiver" android:exported="false" />

View file

@ -8,7 +8,7 @@ buildscript {
google() google()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.1.3' classpath 'com.android.tools.build:gradle:3.1.4'
} }
} }

View file

@ -14,8 +14,8 @@
style="?explainerText" style="?explainerText"
android:textSize="16sp" /> android:textSize="16sp" />
<android.support.v4.view.ViewPager <FrameLayout
android:id="@+id/reading_pager" android:id="@+id/activity_reading_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="10dp"
android:paddingRight="10dp" >
<EditText
android:id="@+id/feed_name_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginTop="5dp"
android:singleLine="true"
/>
</RelativeLayout>

View file

@ -10,7 +10,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
style="?readingBackground" style="?readingBackground"
> >
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -18,9 +18,105 @@
android:orientation="vertical" android:orientation="vertical"
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
> >
<include layout="@layout/include_reading_item_metadata" /> <RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
style="?rowItemHeaderBackground">
<RelativeLayout
android:id="@+id/row_item_feed_header"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:minHeight="6dp"
android:paddingTop="4dp"
android:paddingBottom="4dp" >
<ImageView
android:id="@+id/reading_feed_icon"
android:layout_width="17dp"
android:layout_height="17dp"
android:layout_alignParentLeft="true"
android:layout_marginLeft="16dp"
android:layout_marginRight="5dp"
android:contentDescription="@string/description_row_folder_icon" />
<TextView
android:id="@+id/reading_feed_title"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toRightOf="@id/reading_feed_icon"
android:layout_centerVertical="true"
android:ellipsize="end"
android:lines="1"
android:textSize="12sp"
android:textStyle="bold" />
</RelativeLayout>
<View
android:id="@+id/item_feed_border"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/row_item_feed_header"
android:background="@color/gray55"/>
<Button
android:id="@+id/story_context_menu_button"
android:layout_width="34dp"
android:layout_height="34dp"
android:layout_margin="5dp"
android:background="@drawable/ic_menu_moreoverflow"
android:contentDescription="@string/description_menu"
android:layout_below="@id/item_feed_border"
android:layout_alignParentRight="true" />
<TextView
android:id="@+id/reading_item_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="16dp"
android:layout_marginTop="10dp"
android:layout_marginLeft="16dp"
android:layout_below="@id/item_feed_border"
android:layout_toLeftOf="@id/story_context_menu_button"
android:textSize="17sp" />
<TextView
android:id="@+id/reading_item_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/reading_item_title"
android:layout_alignLeft="@id/reading_item_title"
android:layout_marginRight="12dp"
android:layout_marginTop="8dp"
style="?readingItemMetadata"
android:textSize="12sp" />
<TextView
android:id="@+id/reading_item_authors"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/reading_item_date"
android:layout_toRightOf="@id/reading_item_date"
android:maxLines="1"
android:minWidth="80dp"
style="?readingItemMetadata"
android:textSize="12sp"
android:textStyle="bold"/>
<com.newsblur.view.FlowLayout
android:id="@+id/reading_item_tags"
android:layout_width="match_parent"
android:layout_height="17dp"
android:layout_alignLeft="@id/reading_item_title"
android:layout_below="@id/reading_item_date"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp" />
</RelativeLayout>
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
@ -51,6 +147,20 @@
android:visibility="gone" android:visibility="gone"
/> />
<TextView
android:id="@+id/reading_textmodefailed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:paddingBottom="5dp"
style="?readingBackground"
android:gravity="center_horizontal"
android:text="@string/orig_text_failed"
android:textSize="16sp"
android:textStyle="italic"
android:visibility="gone"
/>
<com.newsblur.view.NewsblurWebview <com.newsblur.view.NewsblurWebview
android:id="@+id/reading_webview" android:id="@+id/reading_webview"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<android.support.v4.view.ViewPager
android:id="@+id/reading_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</FrameLayout>

View file

@ -1,96 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
style="?rowItemHeaderBackground">
<RelativeLayout
android:id="@+id/row_item_feed_header"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:minHeight="6dp"
android:paddingTop="4dp"
android:paddingBottom="4dp" >
<ImageView
android:id="@+id/reading_feed_icon"
android:layout_width="17dp"
android:layout_height="17dp"
android:layout_alignParentLeft="true"
android:layout_marginLeft="16dp"
android:layout_marginRight="5dp"
android:contentDescription="@string/description_row_folder_icon" />
<TextView
android:id="@+id/reading_feed_title"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_toRightOf="@id/reading_feed_icon"
android:layout_centerVertical="true"
android:ellipsize="end"
android:lines="1"
android:textColor="@color/newsblur_blue"
android:textSize="12sp"
android:textStyle="bold" />
</RelativeLayout>
<View
android:id="@+id/item_feed_border"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/row_item_feed_header"
android:background="@color/gray55"/>
<TextView
android:id="@+id/reading_item_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="16dp"
android:layout_marginTop="10dp"
android:layout_marginLeft="16dp"
android:layout_below="@id/item_feed_border"
android:textSize="17sp" />
<TextView
android:id="@+id/reading_item_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/reading_item_title"
android:layout_alignLeft="@id/reading_item_title"
android:layout_marginRight="12dp"
android:layout_marginTop="8dp"
style="?readingItemMetadata"
android:textSize="12sp" />
<TextView
android:id="@+id/reading_item_authors"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/reading_item_date"
android:layout_toRightOf="@id/reading_item_date"
android:maxLines="1"
android:minWidth="80dp"
style="?readingItemMetadata"
android:textSize="12sp"
android:textStyle="bold"/>
<com.newsblur.view.FlowLayout
android:id="@+id/reading_item_tags"
android:layout_width="match_parent"
android:layout_height="17dp"
android:layout_alignLeft="@id/reading_item_title"
android:layout_below="@id/reading_item_date"
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp" />
</RelativeLayout>
</LinearLayout>

View file

@ -13,6 +13,9 @@
<item android:id="@+id/menu_choose_folders" <item android:id="@+id/menu_choose_folders"
android:title="@string/menu_choose_folders" /> android:title="@string/menu_choose_folders" />
<item android:id="@+id/menu_rename_feed"
android:title="@string/menu_rename_feed" />
<item android:id="@+id/menu_notifications" <item android:id="@+id/menu_notifications"
android:title="@string/menu_notifications_choose" > android:title="@string/menu_notifications_choose" >
<menu> <menu>

View file

@ -64,6 +64,9 @@
<item android:id="@+id/menu_delete_feed" <item android:id="@+id/menu_delete_feed"
android:title="@string/menu_delete_feed" android:title="@string/menu_delete_feed"
android:showAsAction="never" /> android:showAsAction="never" />
<item android:id="@+id/menu_rename_feed"
android:title="@string/menu_rename_feed"
android:showAsAction="never" />
<item android:id="@+id/menu_instafetch_feed" <item android:id="@+id/menu_instafetch_feed"
android:title="@string/menu_instafetch_feed" /> android:title="@string/menu_instafetch_feed" />
<item android:id="@+id/menu_infrequent_cutoff" <item android:id="@+id/menu_infrequent_cutoff"

View file

@ -7,63 +7,4 @@
android:showAsAction="always" android:showAsAction="always"
android:title="@string/menu_fullscreen"/> android:title="@string/menu_fullscreen"/>
<item
android:id="@+id/menu_reading_sharenewsblur"
android:showAsAction="never"
android:title="@string/menu_sharenewsblur"/>
<item
android:id="@+id/menu_reading_original"
android:showAsAction="never"
android:title="@string/menu_original"/>
<item
android:id="@+id/menu_send_story"
android:showAsAction="never"
android:title="@string/menu_send_story"/>
<item
android:id="@+id/menu_send_story_full"
android:showAsAction="never"
android:title="@string/menu_send_story_full"/>
<item
android:id="@+id/menu_textsize"
android:showAsAction="never"
android:title="@string/menu_textsize"/>
<item
android:id="@+id/menu_font"
android:showAsAction="never"
android:title="@string/menu_font"/>
<item
android:id="@+id/menu_reading_save"
android:showAsAction="never"
android:title="@string/menu_save_story"/>
<item
android:id="@+id/menu_reading_markunread"
android:showAsAction="never"
android:title="@string/menu_mark_unread"/>
<item
android:id="@+id/menu_intel"
android:showAsAction="never"
android:title="@string/menu_intel"/>
<item android:id="@+id/menu_theme"
android:title="@string/menu_theme_choose" >
<menu>
<group android:checkableBehavior="single">
<item android:id="@+id/menu_theme_light"
android:title="@string/light" />
<item android:id="@+id/menu_theme_dark"
android:title="@string/dark" />
<item android:id="@+id/menu_theme_black"
android:title="@string/black" />
</group>
</menu>
</item>
</menu> </menu>

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/menu_reading_sharenewsblur"
android:showAsAction="never"
android:title="@string/menu_sharenewsblur"/>
<item
android:id="@+id/menu_reading_original"
android:showAsAction="never"
android:title="@string/menu_original"/>
<item
android:id="@+id/menu_send_story"
android:showAsAction="never"
android:title="@string/menu_send_story"/>
<item
android:id="@+id/menu_send_story_full"
android:showAsAction="never"
android:title="@string/menu_send_story_full"/>
<item
android:id="@+id/menu_textsize"
android:showAsAction="never"
android:title="@string/menu_textsize"/>
<item
android:id="@+id/menu_font"
android:showAsAction="never"
android:title="@string/menu_font"/>
<item
android:id="@+id/menu_reading_save"
android:showAsAction="never"
android:title="@string/menu_save_story"/>
<item
android:id="@+id/menu_reading_markunread"
android:showAsAction="never"
android:title="@string/menu_mark_unread"/>
<item
android:id="@+id/menu_intel"
android:showAsAction="never"
android:title="@string/menu_intel"/>
<item android:id="@+id/menu_theme"
android:title="@string/menu_theme_choose" >
<menu>
<group android:checkableBehavior="single">
<item android:id="@+id/menu_theme_light"
android:title="@string/light" />
<item android:id="@+id/menu_theme_dark"
android:title="@string/dark" />
<item android:id="@+id/menu_theme_black"
android:title="@string/black" />
</group>
</menu>
</item>
</menu>

View file

@ -35,6 +35,7 @@
<string name="description_menu">Menu</string> <string name="description_menu">Menu</string>
<string name="title_choose_folders">Choose Folders for Feed %s</string> <string name="title_choose_folders">Choose Folders for Feed %s</string>
<string name="title_rename_feed">Rename Feed %s</string>
<string name="all_stories_row_title">ALL STORIES</string> <string name="all_stories_row_title">ALL STORIES</string>
<string name="all_stories_title">All Stories</string> <string name="all_stories_title">All Stories</string>
@ -60,6 +61,7 @@
<string name="share_this_story">SHARE</string> <string name="share_this_story">SHARE</string>
<string name="unshare">DELETE SHARE</string> <string name="unshare">DELETE SHARE</string>
<string name="update_shared">UPDATE COMMENT</string> <string name="update_shared">UPDATE COMMENT</string>
<string name="feed_name_save">RENAME FEED</string>
<string name="save_this">SAVE</string> <string name="save_this">SAVE</string>
<string name="unsave_this">REMOVE FROM SAVED</string> <string name="unsave_this">REMOVE FROM SAVED</string>
@ -70,7 +72,7 @@
<string name="overlay_count_toast_1">1 unread story</string> <string name="overlay_count_toast_1">1 unread story</string>
<string name="overlay_text">TEXT</string> <string name="overlay_text">TEXT</string>
<string name="overlay_story">STORY</string> <string name="overlay_story">STORY</string>
<string name="text_mode_unavailable">Sorry, the story\'s text could not be extracted.</string> <string name="orig_text_failed">Sorry, the story\'s text could not be extracted.</string>
<string name="state_all">ALL</string> <string name="state_all">ALL</string>
<string name="state_unread">UNREAD</string> <string name="state_unread">UNREAD</string>
@ -156,13 +158,13 @@
<string name="menu_instafetch_feed">Insta-fetch stories</string> <string name="menu_instafetch_feed">Insta-fetch stories</string>
<string name="menu_infrequent_cutoff">Infrequent stories per month</string> <string name="menu_infrequent_cutoff">Infrequent stories per month</string>
<string name="menu_intel">Intelligence trainer</string> <string name="menu_intel">Intelligence trainer</string>
<string name="menu_rename_feed">Rename feed</string>
<string name="menu_story_list_style_choose">List style…</string> <string name="menu_story_list_style_choose">List style…</string>
<string name="list_style_list">List</string> <string name="list_style_list">List</string>
<string name="list_style_grid_f">Grid (fine)</string> <string name="list_style_grid_f">Grid (fine)</string>
<string name="list_style_grid_m">Grid (medium)</string> <string name="list_style_grid_m">Grid (medium)</string>
<string name="list_style_grid_c">Grid (coarse)</string> <string name="list_style_grid_c">Grid (coarse)</string>
<string name="toast_story_unread">Story marked as unread</string>
<string name="toast_hold_to_select">Press and hold to select text</string> <string name="toast_hold_to_select">Press and hold to select text</string>
<string name="logout_warning">Are you sure you want to log out?</string> <string name="logout_warning">Are you sure you want to log out?</string>

View file

@ -1,8 +1,6 @@
package com.newsblur.activity; package com.newsblur.activity;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import com.newsblur.R; import com.newsblur.R;
import com.newsblur.util.UIUtils; import com.newsblur.util.UIUtils;

View file

@ -1,8 +1,6 @@
package com.newsblur.activity; package com.newsblur.activity;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import com.newsblur.R; import com.newsblur.R;
import com.newsblur.util.UIUtils; import com.newsblur.util.UIUtils;

View file

@ -3,13 +3,13 @@ package com.newsblur.activity;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.DialogFragment; import android.support.v4.app.DialogFragment;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import com.newsblur.R; import com.newsblur.R;
import com.newsblur.domain.Feed; import com.newsblur.domain.Feed;
import com.newsblur.fragment.DeleteFeedFragment; import com.newsblur.fragment.DeleteFeedFragment;
import com.newsblur.fragment.FeedIntelTrainerFragment; import com.newsblur.fragment.FeedIntelTrainerFragment;
import com.newsblur.fragment.RenameFeedFragment;
import com.newsblur.util.FeedUtils; import com.newsblur.util.FeedUtils;
import com.newsblur.util.UIUtils; import com.newsblur.util.UIUtils;
@ -66,6 +66,13 @@ public class FeedItemsList extends ItemsList {
intelFrag.show(getSupportFragmentManager(), FeedIntelTrainerFragment.class.getName()); intelFrag.show(getSupportFragmentManager(), FeedIntelTrainerFragment.class.getName());
return true; return true;
} }
if (item.getItemId() == R.id.menu_rename_feed) {
RenameFeedFragment frag = RenameFeedFragment.newInstance(feed);
frag.show(getSupportFragmentManager(), RenameFeedFragment.class.getName());
return true;
// TODO: since this activity uses a feed object passed as an extra and doesn't query the DB,
// the name change won't be reflected until the activity finishes.
}
return false; return false;
} }

View file

@ -6,7 +6,6 @@ import android.view.MenuItem;
import com.newsblur.R; import com.newsblur.R;
import com.newsblur.fragment.InfrequentCutoffDialogFragment; import com.newsblur.fragment.InfrequentCutoffDialogFragment;
import com.newsblur.fragment.InfrequentCutoffDialogFragment.InfrequentCutoffChangedListener; import com.newsblur.fragment.InfrequentCutoffDialogFragment.InfrequentCutoffChangedListener;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.FeedUtils; import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils; import com.newsblur.util.PrefsUtils;
import com.newsblur.util.UIUtils; import com.newsblur.util.UIUtils;

View file

@ -186,6 +186,7 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
menu.findItem(R.id.menu_delete_feed).setVisible(false); menu.findItem(R.id.menu_delete_feed).setVisible(false);
menu.findItem(R.id.menu_instafetch_feed).setVisible(false); menu.findItem(R.id.menu_instafetch_feed).setVisible(false);
menu.findItem(R.id.menu_intel).setVisible(false); menu.findItem(R.id.menu_intel).setVisible(false);
menu.findItem(R.id.menu_rename_feed).setVisible(false);
} }
if (!fs.isInfrequent()) { if (!fs.isInfrequent()) {

View file

@ -1,12 +1,9 @@
package com.newsblur.activity; package com.newsblur.activity;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentActivity;
import android.widget.Toast; import android.widget.Toast;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedUtils; import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils; import com.newsblur.util.PrefsUtils;
import com.newsblur.util.PrefConstants.ThemeValue; import com.newsblur.util.PrefConstants.ThemeValue;
@ -119,8 +116,7 @@ public class NbActivity extends FragmentActivity {
* Pokes the sync service to perform any pending sync actions. * Pokes the sync service to perform any pending sync actions.
*/ */
protected void triggerSync() { protected void triggerSync() {
Intent i = new Intent(this, NBSyncService.class); FeedUtils.triggerSync(this);
startService(i);
} }
/** /**

View file

@ -5,11 +5,10 @@ import java.util.List;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
@ -34,10 +33,7 @@ import com.newsblur.R;
import com.newsblur.database.ReadingAdapter; import com.newsblur.database.ReadingAdapter;
import com.newsblur.domain.Story; import com.newsblur.domain.Story;
import com.newsblur.fragment.ReadingItemFragment; import com.newsblur.fragment.ReadingItemFragment;
import com.newsblur.fragment.ShareDialogFragment; import com.newsblur.fragment.ReadingPagerFragment;
import com.newsblur.fragment.StoryIntelTrainerFragment;
import com.newsblur.fragment.ReadingFontDialogFragment;
import com.newsblur.fragment.TextSizeDialogFragment;
import com.newsblur.service.NBSyncService; import com.newsblur.service.NBSyncService;
import com.newsblur.util.AppConstants; import com.newsblur.util.AppConstants;
import com.newsblur.util.DefaultFeedView; import com.newsblur.util.DefaultFeedView;
@ -93,8 +89,8 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
@Bind(R.id.reading_sync_status) TextView overlayStatusText; @Bind(R.id.reading_sync_status) TextView overlayStatusText;
ViewPager pager; ViewPager pager;
ReadingPagerFragment readingFragment;
protected FragmentManager fragmentManager;
protected ReadingAdapter readingAdapter; protected ReadingAdapter readingAdapter;
private boolean stopLoading; private boolean stopLoading;
protected FeedSet fs; protected FeedSet fs;
@ -126,8 +122,6 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
setContentView(R.layout.activity_reading); setContentView(R.layout.activity_reading);
ButterKnife.bind(this); ButterKnife.bind(this);
fragmentManager = getSupportFragmentManager();
try { try {
fs = (FeedSet)getIntent().getSerializableExtra(EXTRA_FEEDSET); fs = (FeedSet)getIntent().getSerializableExtra(EXTRA_FEEDSET);
} catch (RuntimeException re) { } catch (RuntimeException re) {
@ -185,24 +179,21 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
enableProgressCircle(overlayProgressLeft, false); enableProgressCircle(overlayProgressLeft, false);
enableProgressCircle(overlayProgressRight, false); enableProgressCircle(overlayProgressRight, false);
boolean showFeedMetadata = true; FragmentManager fragmentManager = getSupportFragmentManager();
if (fs.isSingleNormal()) showFeedMetadata = false; ReadingPagerFragment fragment = (ReadingPagerFragment) fragmentManager.findFragmentByTag(ReadingPagerFragment.class.getName());
String sourceUserId = null; if (fragment == null) {
if (fs.getSingleSocialFeed() != null) sourceUserId = fs.getSingleSocialFeed().getKey(); fragment = ReadingPagerFragment.newInstance();
readingAdapter = new ReadingAdapter(getSupportFragmentManager(), sourceUserId, showFeedMetadata); FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.activity_reading_container, fragment, ReadingPagerFragment.class.getName());
setupPager(); transaction.commit();
}
getSupportLoaderManager().initLoader(0, null, this); getSupportLoaderManager().initLoader(0, null, this);
} }
@Override @Override
protected void onSaveInstanceState(Bundle outState) { protected void onSaveInstanceState(Bundle outState) {
/*
TODO: the following call is pretty standard, but causes the pager to disappear when
restoring from a bundle (e.g. after rotation)
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
*/
if (storyHash != null) { if (storyHash != null) {
outState.putString(EXTRA_STORY_HASH, storyHash); outState.putString(EXTRA_STORY_HASH, storyHash);
} else { } else {
@ -273,41 +264,37 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
return; return;
} }
//NB: this implicitly calls readingAdapter.notifyDataSetChanged(); if (readingAdapter != null) {
readingAdapter.swapCursor(cursor); // swapCursor() will asynch process the new cursor and fully update the pager,
// update child fragments, and then call pagerUpdated()
boolean lastCursorWasStale = (stories == null); readingAdapter.swapCursor(cursor, pager);
}
stories = cursor; stories = cursor;
// if the pager previously showed a stale set of stories, it is *not* sufficent to just
// swap out the cursor and invalidate. no number of calls to notifyDataSetChanged() or
// setCurrentItem() will ever get a pager to refresh the currently displayed fragment.
// however, the pager can be tricked into wiping all fragments and recreating them from
// the adapter by setting the adapter again, even if it is the same one.
if (lastCursorWasStale) {
// TODO: can crash with IllegalStateException
pager.setAdapter(readingAdapter);
}
com.newsblur.util.Log.d(this.getClass().getName(), "loaded cursor with count: " + cursor.getCount()); com.newsblur.util.Log.d(this.getClass().getName(), "loaded cursor with count: " + cursor.getCount());
if (cursor.getCount() < 1) { if (cursor.getCount() < 1) {
triggerRefresh(AppConstants.READING_STORY_PRELOAD); triggerRefresh(AppConstants.READING_STORY_PRELOAD);
} }
// see if we are just starting and need to jump to a target story
skipPagerToStoryHash();
if (unreadSearchActive) {
// if we left this flag high, we were looking for an unread, but didn't find one;
// now that we have more stories, look again.
nextUnread();
}
updateOverlayNav();
updateOverlayText();
} }
} }
/**
* notify the activity that the dataset for the pager has fully been updated
*/
public void pagerUpdated() {
// see if we are just starting and need to jump to a target story
skipPagerToStoryHash();
if (unreadSearchActive) {
// if we left this flag high, we were looking for an unread, but didn't find one;
// now that we have more stories, look again.
nextUnread();
}
updateOverlayNav();
updateOverlayText();
}
private void skipPagerToStoryHash() { private void skipPagerToStoryHash() {
// if we already started and found our target story, this will be unset // if we already started and found our target story, this will be unset
if (storyHash == null) return; if (storyHash == null) return;
@ -321,8 +308,6 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
if (stopLoading) return; if (stopLoading) return;
if (position >= 0 ) { if (position >= 0 ) {
// see above note about re-setting the adapter to force the pager to reload fragments
pager.setAdapter(readingAdapter);
pager.setCurrentItem(position, false); pager.setCurrentItem(position, false);
this.onPageSelected(position); this.onPageSelected(position);
// now that the pager is getting the right story, make it visible // now that the pager is getting the right story, make it visible
@ -336,8 +321,16 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
this.checkStoryCount(readingAdapter.getCount()+1); this.checkStoryCount(readingAdapter.getCount()+1);
} }
private void setupPager() { /*
pager = (ViewPager) findViewById(R.id.reading_pager); * The key component of this activity is the pager, which in order to correctly use
* child fragments for stories, needs to be within an enclosing fragment. Because
* the view heirarchy of that fragment will have a different lifecycle than the
* activity, we need a way to get access to the pager when it is created and only
* then can we set it up.
*/
public void offerPager(ViewPager pager, FragmentManager childFragmentManager) {
this.pager = pager;
// since it might start on the wrong story, create the pager as invisible // since it might start on the wrong story, create the pager as invisible
pager.setVisibility(View.INVISIBLE); pager.setVisibility(View.INVISIBLE);
pager.setPageMargin(UIUtils.dp2px(getApplicationContext(), 1)); pager.setPageMargin(UIUtils.dp2px(getApplicationContext(), 1));
@ -349,7 +342,12 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
pager.setPageMarginDrawable(R.drawable.divider_dark); pager.setPageMarginDrawable(R.drawable.divider_dark);
} }
pager.addOnPageChangeListener(this); boolean showFeedMetadata = true;
if (fs.isSingleNormal()) showFeedMetadata = false;
String sourceUserId = null;
if (fs.getSingleSocialFeed() != null) sourceUserId = fs.getSingleSocialFeed().getKey();
readingAdapter = new ReadingAdapter(childFragmentManager, sourceUserId, showFeedMetadata, this);
pager.setAdapter(readingAdapter); pager.setAdapter(readingAdapter);
// if the first story in the list was "viewed" before the page change listener was set, // if the first story in the list was "viewed" before the page change listener was set,
@ -384,91 +382,17 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
@Override @Override
public boolean onPrepareOptionsMenu(Menu menu) { public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu); super.onPrepareOptionsMenu(menu);
if (readingAdapter == null || pager == null) { return false; }
Story story = readingAdapter.getStory(pager.getCurrentItem());
if (story == null ) { return false; }
menu.findItem(R.id.menu_reading_save).setTitle(story.starred ? R.string.menu_unsave_story : R.string.menu_save_story);
if (fs.isFilterSaved() || fs.isAllSaved() || (fs.getSingleSavedTag() != null)) menu.findItem(R.id.menu_reading_markunread).setVisible(false);
ThemeValue themeValue = PrefsUtils.getSelectedTheme(this);
if (themeValue == ThemeValue.LIGHT) {
menu.findItem(R.id.menu_theme_light).setChecked(true);
} else if (themeValue == ThemeValue.DARK) {
menu.findItem(R.id.menu_theme_dark).setChecked(true);
} else if (themeValue == ThemeValue.BLACK) {
menu.findItem(R.id.menu_theme_black).setChecked(true);
}
return true; return true;
} }
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
if (pager == null) return false;
int currentItem = pager.getCurrentItem();
Story story = readingAdapter.getStory(currentItem);
if (story == null) return false;
if (item.getItemId() == android.R.id.home) { if (item.getItemId() == android.R.id.home) {
finish(); finish();
return true; return true;
} else if (item.getItemId() == R.id.menu_reading_original) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(story.permalink));
try {
startActivity(i);
} catch (Exception e) {
android.util.Log.wtf(this.getClass().getName(), "device cannot open URLs");
}
return true;
} else if (item.getItemId() == R.id.menu_reading_sharenewsblur) {
DialogFragment newFragment = ShareDialogFragment.newInstance(story, readingAdapter.getSourceUserId());
newFragment.show(getSupportFragmentManager(), "dialog");
return true;
} else if (item.getItemId() == R.id.menu_send_story) {
FeedUtils.sendStoryBrief(story, this);
return true;
} else if (item.getItemId() == R.id.menu_send_story_full) {
FeedUtils.sendStoryFull(story, this);
return true;
} else if (item.getItemId() == R.id.menu_textsize) {
TextSizeDialogFragment textSize = TextSizeDialogFragment.newInstance(PrefsUtils.getTextSize(this), TextSizeDialogFragment.TextSizeType.ReadingText);
textSize.show(getSupportFragmentManager(), TextSizeDialogFragment.class.getName());
return true;
} else if (item.getItemId() == R.id.menu_font) {
ReadingFontDialogFragment storyFont = ReadingFontDialogFragment.newInstance(PrefsUtils.getFontString(this));
storyFont.show(getSupportFragmentManager(), ReadingFontDialogFragment.class.getName());
return true;
} else if (item.getItemId() == R.id.menu_reading_save) {
if (story.starred) {
FeedUtils.setStorySaved(story, false, Reading.this);
} else {
FeedUtils.setStorySaved(story, true, Reading.this);
}
return true;
} else if (item.getItemId() == R.id.menu_reading_markunread) {
this.markStoryUnread(story);
return true;
} else if (item.getItemId() == R.id.menu_reading_fullscreen) { } else if (item.getItemId() == R.id.menu_reading_fullscreen) {
ViewUtils.hideSystemUI(getWindow().getDecorView()); ViewUtils.hideSystemUI(getWindow().getDecorView());
return true; return true;
} else if (item.getItemId() == R.id.menu_theme_light) {
PrefsUtils.setSelectedTheme(this, ThemeValue.LIGHT);
UIUtils.restartActivity(this);
return true;
} else if (item.getItemId() == R.id.menu_theme_dark) {
PrefsUtils.setSelectedTheme(this, ThemeValue.DARK);
UIUtils.restartActivity(this);
return true;
} else if (item.getItemId() == R.id.menu_theme_black) {
PrefsUtils.setSelectedTheme(this, ThemeValue.BLACK);
UIUtils.restartActivity(this);
return true;
} else if (item.getItemId() == R.id.menu_intel) {
if (story.feedId.equals("0")) return true; // cannot train on feedless stories
StoryIntelTrainerFragment intelFrag = StoryIntelTrainerFragment.newInstance(story, fs);
intelFrag.show(getSupportFragmentManager(), StoryIntelTrainerFragment.class.getName());
return true;
} else { } else {
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@ -534,7 +458,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
if (readingAdapter == null) return null; if (readingAdapter == null) return null;
Story story = readingAdapter.getStory(position); Story story = readingAdapter.getStory(position);
if (story != null) { if (story != null) {
markStoryRead(story); FeedUtils.markStoryAsRead(story, Reading.this);
synchronized (pageHistory) { synchronized (pageHistory) {
// if the history is just starting out or the last entry in it isn't this page, add this page // if the history is just starting out or the last entry in it isn't this page, add this page
if ((pageHistory.size() < 1) || (!story.equals(pageHistory.get(pageHistory.size()-1)))) { if ((pageHistory.size() < 1) || (!story.equals(pageHistory.get(pageHistory.size()-1)))) {
@ -720,15 +644,6 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
} }
} }
private void markStoryRead(Story story) {
FeedUtils.markStoryAsRead(story, this);
}
private void markStoryUnread(Story story) {
FeedUtils.markStoryUnread(story, this);
Toast.makeText(Reading.this, R.string.toast_story_unread, Toast.LENGTH_SHORT).show();
}
// NB: this callback is for the text size slider // NB: this callback is for the text size slider
@Override @Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {

View file

@ -1532,6 +1532,12 @@ public class BlurDatabaseHelper {
return result; return result;
} }
public void renameFeed(String feedId, String newFeedName) {
ContentValues values = new ContentValues();
values.put(DatabaseConstants.FEED_TITLE, newFeedName);
synchronized (RW_MUTEX) {dbRW.update(DatabaseConstants.FEED_TABLE, values, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});}
}
public static void closeQuietly(Cursor c) { public static void closeQuietly(Cursor c) {
if (c == null) return; if (c == null) return;
try {c.close();} catch (Exception e) {;} try {c.close();} catch (Exception e) {;}

View file

@ -138,21 +138,7 @@ public class DatabaseConstants {
public static final String ACTION_ID = BaseColumns._ID; public static final String ACTION_ID = BaseColumns._ID;
public static final String ACTION_TIME = "time"; public static final String ACTION_TIME = "time";
public static final String ACTION_TRIED = "tried"; public static final String ACTION_TRIED = "tried";
public static final String ACTION_TYPE = "action_type"; public static final String ACTION_PARAMS = "action_params";
public static final String ACTION_COMMENT_TEXT = "comment_text";
public static final String ACTION_REPLY_ID = "reply_id";
public static final String ACTION_STORY_HASH = "story_hash";
public static final String ACTION_FEED_ID = "feed_id";
public static final String ACTION_FEED_SET = "feed_set";
public static final String ACTION_MODIFIED_FEED_IDS = "modified_feed_ids";
public static final String ACTION_INCLUDE_OLDER = "include_older";
public static final String ACTION_INCLUDE_NEWER = "include_newer";
public static final String ACTION_STORY_ID = "story_id";
public static final String ACTION_SOURCE_USER_ID = "source_user_id";
public static final String ACTION_COMMENT_ID = "comment_id";
public static final String ACTION_NOTIFY_FILTER = "notify_filter";
public static final String ACTION_NOTIFY_TYPES = "notify_types";
public static final String ACTION_CLASSIFIER = "classifier";
public static final String STARREDCOUNTS_TABLE = "starred_counts"; public static final String STARREDCOUNTS_TABLE = "starred_counts";
public static final String STARREDCOUNTS_COUNT = "count"; public static final String STARREDCOUNTS_COUNT = "count";
@ -302,21 +288,7 @@ public class DatabaseConstants {
ACTION_ID + INTEGER + " PRIMARY KEY AUTOINCREMENT, " + ACTION_ID + INTEGER + " PRIMARY KEY AUTOINCREMENT, " +
ACTION_TIME + INTEGER + " NOT NULL, " + ACTION_TIME + INTEGER + " NOT NULL, " +
ACTION_TRIED + INTEGER + ", " + ACTION_TRIED + INTEGER + ", " +
ACTION_TYPE + TEXT + ", " + ACTION_PARAMS + TEXT +
ACTION_COMMENT_TEXT + TEXT + ", " +
ACTION_STORY_HASH + TEXT + ", " +
ACTION_FEED_ID + TEXT + ", " +
ACTION_FEED_SET + TEXT + ", " +
ACTION_INCLUDE_OLDER + INTEGER + ", " +
ACTION_INCLUDE_NEWER + INTEGER + ", " +
ACTION_STORY_ID + TEXT + ", " +
ACTION_SOURCE_USER_ID + TEXT + ", " +
ACTION_COMMENT_ID + TEXT + ", " +
ACTION_REPLY_ID + TEXT + ", " +
ACTION_MODIFIED_FEED_IDS + TEXT + ", " +
ACTION_NOTIFY_FILTER + TEXT + ", " +
ACTION_NOTIFY_TYPES + TEXT + ", " +
ACTION_CLASSIFIER + TEXT +
")"; ")";
static final String STARREDCOUNTS_SQL = "CREATE TABLE " + STARREDCOUNTS_TABLE + " (" + static final String STARREDCOUNTS_SQL = "CREATE TABLE " + STARREDCOUNTS_TABLE + " (" +

View file

@ -1,114 +1,258 @@
package com.newsblur.database; package com.newsblur.database;
import android.database.Cursor; import android.database.Cursor;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.app.FragmentTransaction;
import android.util.SparseArray; import android.support.v4.view.PagerAdapter;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.newsblur.activity.NbActivity; import com.newsblur.activity.NbActivity;
import com.newsblur.activity.Reading;
import com.newsblur.domain.Classifier; import com.newsblur.domain.Classifier;
import com.newsblur.domain.Story; import com.newsblur.domain.Story;
import com.newsblur.fragment.LoadingFragment; import com.newsblur.fragment.LoadingFragment;
import com.newsblur.fragment.ReadingItemFragment; import com.newsblur.fragment.ReadingItemFragment;
import com.newsblur.util.FeedUtils; import com.newsblur.util.FeedUtils;
import java.lang.ref.WeakReference; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ReadingAdapter extends FragmentStatePagerAdapter { /**
* An adapter to display stories in a ViewPager. Loosely based upon FragmentStatePagerAdapter, but
* with enhancements to correctly handle item insertion / removal and to pass invalidation down
* to child fragments during updates.
*/
public class ReadingAdapter extends PagerAdapter {
private Cursor stories;
private String sourceUserId; private String sourceUserId;
private boolean showFeedMetadata; private boolean showFeedMetadata;
private SparseArray<WeakReference<ReadingItemFragment>> cachedFragments; private Reading activity;
private FragmentManager fm;
public ReadingAdapter(FragmentManager fm, String sourceUserId, boolean showFeedMetadata) { private FragmentTransaction curTransaction = null;
super(fm); private Fragment lastActiveFragment = null;
this.cachedFragments = new SparseArray<WeakReference<ReadingItemFragment>>(); private HashMap<String,ReadingItemFragment> fragments;
private HashMap<String,Fragment.SavedState> states;
// the cursor from which we pull story objects. should not be used except by the thaw worker
private Cursor mostRecentCursor;
// the live list of stories being used by the adapter
private List<Story> stories = new ArrayList<Story>(0);
// classifiers for each feed seen in the story list
private Map<String,Classifier> classifiers = new HashMap<String,Classifier>(0);
private final ExecutorService executorService;
public ReadingAdapter(FragmentManager fm, String sourceUserId, boolean showFeedMetadata, Reading activity) {
this.sourceUserId = sourceUserId; this.sourceUserId = sourceUserId;
this.showFeedMetadata = showFeedMetadata; this.showFeedMetadata = showFeedMetadata;
this.fm = fm;
this.activity = activity;
this.fragments = new HashMap<String,ReadingItemFragment>();
this.states = new HashMap<String,Fragment.SavedState>();
executorService = Executors.newFixedThreadPool(1);
}
public void swapCursor(final Cursor c, final View v) {
// cache the identity of the most recent cursor so async batches can check to
// see if they are stale
mostRecentCursor = c;
// process the cursor into objects and update the View async
Runnable r = new Runnable() {
@Override
public void run() {
thaw(c, v);
}
};
executorService.submit(r);
}
/**
* Attempt to thaw a new set of stories from the cursor most recently
* seen when the that cycle started.
*/
private void thaw(final Cursor c, View v) {
if (c != mostRecentCursor) return;
// thawed stories
final List<Story> newStories;
// attempt to thaw as gracefully as possible despite the fact that the loader
// framework could close our cursor at any moment. if this happens, it is fine,
// as a new one will be provided and another cycle will start. just return.
try {
if (c == null) {
newStories = new ArrayList<Story>(0);
} else {
if (c.isClosed()) return;
newStories = new ArrayList<Story>(c.getCount());
// keep track of which feeds are in this story set so we can also fetch Classifiers
Set<String> feedIdsSeen = new HashSet<String>();
c.moveToPosition(-1);
while (c.moveToNext()) {
if (c.isClosed()) return;
Story s = Story.fromCursor(c);
s.bindExternValues(c);
newStories.add(s);
feedIdsSeen.add(s.feedId);
}
for (String feedId : feedIdsSeen) {
classifiers.put(feedId, FeedUtils.dbHelper.getClassifierForFeed(feedId));
}
}
} catch (Exception e) {
// because we use interruptable loaders that auto-close cursors, it is expected
// that cursors will sometimes go bad. this is a useful signal to stop the thaw
// thread and let it start on a fresh cursor.
com.newsblur.util.Log.e(this, "error thawing story list: " + e.getMessage(), e);
return;
}
if (c != mostRecentCursor) return;
v.post(new Runnable() {
@Override
public void run() {
if (c != mostRecentCursor) return;
stories = newStories;
notifyDataSetChanged();
activity.pagerUpdated();
}
});
}
public Story getStory(int position) {
if (position >= stories.size() || position < 0) {
return null;
} else {
return stories.get(position);
}
}
@Override
public int getCount() {
return stories.size();
} }
@Override private ReadingItemFragment createFragment(Story story) {
public synchronized Fragment getItem(int position) { return ReadingItemFragment.newInstance(story,
if (stories == null || stories.getCount() == 0 || position >= stories.getCount()) { story.extern_feedTitle,
return new LoadingFragment(); story.extern_feedColor,
} else { story.extern_feedFade,
stories.moveToPosition(position); story.extern_faviconBorderColor,
Story story = Story.fromCursor(stories); story.extern_faviconTextColor,
story.bindExternValues(stories); story.extern_faviconUrl,
classifiers.get(story.feedId),
// TODO: does the pager generate new fragments in the UI thread? If so, classifiers should showFeedMetadata,
// be loaded async by the fragment itself sourceUserId);
Classifier classifier = FeedUtils.dbHelper.getClassifierForFeed(story.feedId);
return ReadingItemFragment.newInstance(story,
story.extern_feedTitle,
story.extern_feedColor,
story.extern_feedFade,
story.extern_faviconBorderColor,
story.extern_faviconTextColor,
story.extern_faviconUrl,
classifier,
showFeedMetadata,
sourceUserId);
}
} }
@Override @Override
public Object instantiateItem(ViewGroup container, int position) { public Object instantiateItem(ViewGroup container, int position) {
Object o = super.instantiateItem(container, position); Story story = getStory(position);
if (o instanceof ReadingItemFragment) { Fragment fragment = null;
cachedFragments.put(position, new WeakReference((ReadingItemFragment) o)); if (story == null) {
fragment = new LoadingFragment();
} else {
fragment = fragments.get(story.storyHash);
if (fragment == null) {
ReadingItemFragment rif = createFragment(story);
fragment = rif;
Fragment.SavedState oldState = states.get(story.storyHash);
if (oldState != null) fragment.setInitialSavedState(oldState);
fragments.put(story.storyHash, rif);
} else {
// iff there was a real fragment for this story already, it will have been added and ready
return fragment;
}
} }
return o; fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
if (curTransaction == null) {
curTransaction = fm.beginTransaction();
}
curTransaction.add(container.getId(), fragment);
return fragment;
} }
@Override @Override
public void destroyItem(ViewGroup container, int position, Object object) { public void destroyItem(ViewGroup container, int position, Object object) {
cachedFragments.remove(position); Fragment fragment = (Fragment) object;
try { if (curTransaction == null) {
super.destroyItem(container, position, object); curTransaction = fm.beginTransaction();
} catch (IllegalStateException ise) { }
// it appears that sometimes the pager impatiently deletes stale fragments befre curTransaction.remove(fragment);
// even calling it's own destroyItem method. we're just passing up the stack if (fragment instanceof ReadingItemFragment) {
// after evicting our cache, so don't expose this internal bug from our call stack ReadingItemFragment rif = (ReadingItemFragment) fragment;
com.newsblur.util.Log.w(this, "ViewPager adapter rejected own destruction call."); if (rif.isAdded()) {
states.put(rif.story.storyHash, fm.saveFragmentInstanceState(rif));
}
fragments.remove(rif.story.storyHash);
} }
} }
public synchronized void swapCursor(Cursor cursor) { @Override
this.stories = cursor; public void setPrimaryItem(ViewGroup container, int position, Object object) {
notifyDataSetChanged(); Fragment fragment = (Fragment) object;
if (fragment != lastActiveFragment) {
if (lastActiveFragment != null) {
lastActiveFragment.setMenuVisibility(false);
lastActiveFragment.setUserVisibleHint(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
}
lastActiveFragment = fragment;
}
} }
@Override
public synchronized int getCount() {
if (stories != null && stories.getCount() > 0) {
return stories.getCount();
} else {
return 1;
}
}
public synchronized Story getStory(int position) { @Override
if (stories == null || stories.isClosed() || stories.getColumnCount() == 0 || position >= stories.getCount() || position < 0) { public void finishUpdate(ViewGroup container) {
return null; if (curTransaction != null) {
} else { curTransaction.commitNowAllowingStateLoss();
stories.moveToPosition(position); curTransaction = null;
Story story = Story.fromCursor(stories); }
return story; }
}
}
public synchronized int getPosition(Story story) { @Override
if (stories == null) return -1; public boolean isViewFromObject(View view, Object object) {
if (stories.isClosed()) return -1; return ((Fragment)object).getView() == view;
}
/**
* get the number of stories we very likely have, even if they haven't
* been thawed yet, for callers that absolutely must know the size
* of our dataset (such as for calculating when to fetch more stories)
*/
public int getRawStoryCount() {
if (mostRecentCursor == null) return 0;
if (mostRecentCursor.isClosed()) return 0;
int count = 0;
try {
count = mostRecentCursor.getCount();
} catch (Exception e) {
// rather than worry about sync locking for cursor changes, just fail. a
// closing cursor may as well not be loaded.
}
return count;
}
public int getPosition(Story story) {
int pos = 0; int pos = 0;
while (pos < stories.getCount()) { while (pos < stories.size()) {
stories.moveToPosition(pos); if (stories.get(pos).equals(story)) {
if (Story.fromCursor(stories).equals(story)) {
return pos; return pos;
} }
pos++; pos++;
@ -117,56 +261,96 @@ public class ReadingAdapter extends FragmentStatePagerAdapter {
} }
@Override @Override
public synchronized int getItemPosition(Object object) { public int getItemPosition(Object object) {
if (object instanceof LoadingFragment) { if (object instanceof ReadingItemFragment) {
return POSITION_NONE; ReadingItemFragment rif = (ReadingItemFragment) object;
} else { int pos = findHash(rif.story.storyHash);
return POSITION_UNCHANGED; if (pos >=0) return pos;
} }
return POSITION_NONE;
} }
public String getSourceUserId() { public ReadingItemFragment getExistingItem(int pos) {
return sourceUserId; Story story = getStory(pos);
} if (story == null) return null;
return fragments.get(story.storyHash);
public synchronized ReadingItemFragment getExistingItem(int pos) {
WeakReference<ReadingItemFragment> frag = cachedFragments.get(pos);
if (frag == null) return null;
return frag.get();
} }
@Override @Override
public synchronized void notifyDataSetChanged() { public void notifyDataSetChanged() {
super.notifyDataSetChanged(); super.notifyDataSetChanged();
// go one step further than the default pageradapter and also refresh the // go one step further than the default pageradapter and also refresh the
// story object inside each fragment we have active // story object inside each fragment we have active
if (stories == null) return; for (Story s : stories) {
for (int i=0; i<stories.getCount(); i++) { ReadingItemFragment rif = fragments.get(s.storyHash);
WeakReference<ReadingItemFragment> frag = cachedFragments.get(i); if (rif != null ) {
if (frag == null) continue; rif.offerStoryUpdate(s);
ReadingItemFragment rif = frag.get(); rif.handleUpdate(NbActivity.UPDATE_STORY);
if (rif == null) continue; }
rif.offerStoryUpdate(getStory(i));
rif.handleUpdate(NbActivity.UPDATE_STORY);
} }
} }
public synchronized int findFirstUnread() { public int findFirstUnread() {
stories.moveToPosition(-1); int pos = 0;
while (stories.moveToNext()) { while (pos < stories.size()) {
Story story = Story.fromCursor(stories); if (! stories.get(pos).read) {
if (!story.read) return stories.getPosition(); return pos;
}
pos++;
} }
return -1; return -1;
} }
public synchronized int findHash(String storyHash) { public int findHash(String storyHash) {
stories.moveToPosition(-1); int pos = 0;
while (stories.moveToNext()) { while (pos < stories.size()) {
Story story = Story.fromCursor(stories); if (stories.get(pos).storyHash.equals(storyHash)) {
if (story.storyHash.equals(storyHash)) return stories.getPosition(); return pos;
}
pos++;
} }
return -1; return -1;
} }
@Override
public Parcelable saveState() {
// collect state from any active fragments alongside already-frozen ones
for (Map.Entry<String,ReadingItemFragment> entry : fragments.entrySet()) {
Fragment f = entry.getValue();
if (f.isAdded()) {
states.put(entry.getKey(), fm.saveFragmentInstanceState(f));
}
}
Bundle state = new Bundle();
for (Map.Entry<String,Fragment.SavedState> entry : states.entrySet()) {
state.putParcelable("ss-" + entry.getKey(), entry.getValue());
}
return state;
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
// most FragmentManager impls. will re-create added fragments even if they
// are not set to retaininstance. we want to only save state, not objects,
// so before we start restoration, clear out any stale instances. without
// this, the pager will leak fragments on rotation or context switch.
for (Fragment fragment : fm.getFragments()) {
if (fragment instanceof ReadingItemFragment) {
fm.beginTransaction().remove(fragment).commit();
}
}
Bundle bundle = (Bundle)state;
bundle.setClassLoader(loader);
fragments.clear();
states.clear();
for (String key : bundle.keySet()) {
if (key.startsWith("ss-")) {
String storyHash = key.substring(3);
Parcelable fragState = bundle.getParcelable(key);
states.put(storyHash, (Fragment.SavedState) fragState);
}
}
}
} }

View file

@ -262,6 +262,7 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
if (currentState == StateFilter.SAVED) break; if (currentState == StateFilter.SAVED) break;
inflater.inflate(R.menu.context_feed, menu); inflater.inflate(R.menu.context_feed, menu);
if (adapter.isRowAllSharedStories(groupPosition)) { if (adapter.isRowAllSharedStories(groupPosition)) {
// social feeds
menu.removeItem(R.id.menu_delete_feed); menu.removeItem(R.id.menu_delete_feed);
menu.removeItem(R.id.menu_choose_folders); menu.removeItem(R.id.menu_choose_folders);
menu.removeItem(R.id.menu_unmute_feed); menu.removeItem(R.id.menu_unmute_feed);
@ -269,7 +270,9 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
menu.removeItem(R.id.menu_notifications); menu.removeItem(R.id.menu_notifications);
menu.removeItem(R.id.menu_instafetch_feed); menu.removeItem(R.id.menu_instafetch_feed);
menu.removeItem(R.id.menu_intel); menu.removeItem(R.id.menu_intel);
menu.removeItem(R.id.menu_rename_feed);
} else { } else {
// normal feeds
menu.removeItem(R.id.menu_unfollow); menu.removeItem(R.id.menu_unfollow);
Feed feed = adapter.getFeed(groupPosition, childPosition); Feed feed = adapter.getFeed(groupPosition, childPosition);
@ -352,6 +355,12 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
DialogFragment chooseFoldersFragment = ChooseFoldersFragment.newInstance(feed); DialogFragment chooseFoldersFragment = ChooseFoldersFragment.newInstance(feed);
chooseFoldersFragment.show(getFragmentManager(), "dialog"); chooseFoldersFragment.show(getFragmentManager(), "dialog");
} }
} else if (item.getItemId() == R.id.menu_rename_feed) {
Feed feed = adapter.getFeed(groupPosition, childPosition);
if (feed != null) {
DialogFragment renameFeedFragment = RenameFeedFragment.newInstance(feed);
renameFeedFragment.show(getFragmentManager(), "dialog");
}
} else if (item.getItemId() == R.id.menu_mute_feed) { } else if (item.getItemId() == R.id.menu_mute_feed) {
Set<String> feedIds = new HashSet<String>(); Set<String> feedIds = new HashSet<String>();
feedIds.add(adapter.getFeed(groupPosition, childPosition).feedId); feedIds.add(adapter.getFeed(groupPosition, childPosition).feedId);

View file

@ -159,11 +159,7 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
calcColumnCount(listStyle); calcColumnCount(listStyle);
layoutManager = new GridLayoutManager(getActivity(), columnCount); layoutManager = new GridLayoutManager(getActivity(), columnCount);
itemGrid.setLayoutManager(layoutManager); itemGrid.setLayoutManager(layoutManager);
RecyclerView.ItemAnimator anim = itemGrid.getItemAnimator(); setupAnimSpeeds();
// we try to avoid mid-list updates or pushdowns, but in case they happen, smooth them
// out to avoid rows dodging out from under taps
anim.setAddDuration((long) (anim.getAddDuration() * 1.75));
anim.setMoveDuration((long) (anim.getMoveDuration() * 1.75));
calcGridSpacing(listStyle); calcGridSpacing(listStyle);
itemGrid.addItemDecoration(new RecyclerView.ItemDecoration() { itemGrid.addItemDecoration(new RecyclerView.ItemDecoration() {
@ -227,6 +223,8 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
public void storyThawCompleted(int indexOfLastUnread) { public void storyThawCompleted(int indexOfLastUnread) {
this.indexOfLastUnread = indexOfLastUnread; this.indexOfLastUnread = indexOfLastUnread;
this.fullFlingComplete = false; this.fullFlingComplete = false;
// we don't actually calculate list speed until it has some stories
setupAnimSpeeds();
} }
public void scrollToTop() { public void scrollToTop() {
@ -399,6 +397,25 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
} }
} }
private void setupAnimSpeeds() {
// to mitigate taps missed because of list pushdowns, RVs animate them. however, the speed
// is device and settings dependent. to keep the UI consistent across installs, take the
// system default speed and tweak it towards a speed that looked and functioned well in
// testing while still somewhat respecting the system's requested adjustments to speed.
long targetAddDuration = 250L;
// moves are especially jarring, and very rare
long targetMovDuration = 500L;
// if there are no stories in the list at all, let the first insert happen very quickly
if ((adapter == null) || (adapter.getRawStoryCount() < 1)) {
targetAddDuration = 0L;
targetMovDuration = 0L;
}
RecyclerView.ItemAnimator anim = itemGrid.getItemAnimator();
anim.setAddDuration((long) ((anim.getAddDuration() + targetAddDuration)/2L));
anim.setMoveDuration((long) ((anim.getMoveDuration() + targetMovDuration)/2L));
}
private void onScrolled(RecyclerView recyclerView, int dx, int dy) { private void onScrolled(RecyclerView recyclerView, int dx, int dy) {
// the framework likes to trigger this on init before we even known counts, so disregard those // the framework likes to trigger this on init before we even known counts, so disregard those
if (!cursorSeenYet) return; if (!cursorSeenYet) return;

View file

@ -16,7 +16,6 @@ import android.widget.Toast;
import com.newsblur.R; import com.newsblur.R;
import com.newsblur.activity.Main; import com.newsblur.activity.Main;
import com.newsblur.network.APIManager; import com.newsblur.network.APIManager;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.PrefsUtils; import com.newsblur.util.PrefsUtils;
import com.newsblur.util.UIUtils; import com.newsblur.util.UIUtils;
@ -80,7 +79,6 @@ public class LoginAsDialogFragment extends DialogFragment {
@Override @Override
protected void onPostExecute(Boolean result) { protected void onPostExecute(Boolean result) {
if (result) { if (result) {
NBSyncService.resumeFromInterrupt();
Intent startMain = new Intent(activity, Main.class); Intent startMain = new Intent(activity, Main.class);
activity.startActivity(startMain); activity.startActivity(startMain);
} else { } else {

View file

@ -1,12 +1,9 @@
package com.newsblur.fragment; package com.newsblur.fragment;
import android.app.Activity; import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import com.newsblur.service.NBSyncService; import com.newsblur.util.FeedUtils;
import com.newsblur.util.AppConstants;
public class NbFragment extends Fragment { public class NbFragment extends Fragment {
@ -16,21 +13,8 @@ public class NbFragment extends Fragment {
protected void triggerSync() { protected void triggerSync() {
Activity a = getActivity(); Activity a = getActivity();
if (a != null) { if (a != null) {
Intent i = new Intent(a, NBSyncService.class); FeedUtils.triggerSync(a);
a.startService(i);
} }
} }
@Override
public void onActivityCreated(Bundle savedInstanceState) {
com.newsblur.util.Log.d(this, "onActivityCreated");
super.onActivityCreated(savedInstanceState);
}
@Override
public void onResume() {
com.newsblur.util.Log.d(this, "onResume");
super.onResume();
}
} }

View file

@ -19,6 +19,8 @@ import android.util.Log;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.GestureDetector; import android.view.GestureDetector;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
@ -26,8 +28,9 @@ import android.view.ViewGroup;
import android.webkit.WebView.HitTestResult; import android.webkit.WebView.HitTestResult;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.Bind; import butterknife.Bind;
@ -40,7 +43,6 @@ import com.newsblur.domain.Classifier;
import com.newsblur.domain.Story; import com.newsblur.domain.Story;
import com.newsblur.domain.UserDetails; import com.newsblur.domain.UserDetails;
import com.newsblur.service.OriginalTextService; import com.newsblur.service.OriginalTextService;
import com.newsblur.util.AppConstants;
import com.newsblur.util.DefaultFeedView; import com.newsblur.util.DefaultFeedView;
import com.newsblur.util.FeedSet; import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils; import com.newsblur.util.FeedUtils;
@ -59,8 +61,9 @@ import java.util.HashMap;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class ReadingItemFragment extends NbFragment { public class ReadingItemFragment extends NbFragment implements PopupMenu.OnMenuItemClickListener {
private static final String BUNDLE_SCROLL_POS_REL = "scrollStateRel";
public static final String TEXT_SIZE_CHANGED = "textSizeChanged"; public static final String TEXT_SIZE_CHANGED = "textSizeChanged";
public static final String TEXT_SIZE_VALUE = "textSizeChangeValue"; public static final String TEXT_SIZE_VALUE = "textSizeChangeValue";
public static final String READING_FONT_CHANGED = "readingFontChanged"; public static final String READING_FONT_CHANGED = "readingFontChanged";
@ -71,7 +74,7 @@ public class ReadingItemFragment extends NbFragment {
private Classifier classifier; private Classifier classifier;
@Bind(R.id.reading_webview) NewsblurWebview web; @Bind(R.id.reading_webview) NewsblurWebview web;
@Bind(R.id.custom_view_container) ViewGroup webviewCustomViewLayout; @Bind(R.id.custom_view_container) ViewGroup webviewCustomViewLayout;
@Bind(R.id.reading_scrollview) View fragmentScrollview; @Bind(R.id.reading_scrollview) ScrollView fragmentScrollview;
private BroadcastReceiver textSizeReceiver, readingFontReceiver; private BroadcastReceiver textSizeReceiver, readingFontReceiver;
@Bind(R.id.reading_item_title) TextView itemTitle; @Bind(R.id.reading_item_title) TextView itemTitle;
@Bind(R.id.reading_item_authors) TextView itemAuthors; @Bind(R.id.reading_item_authors) TextView itemAuthors;
@ -83,8 +86,10 @@ public class ReadingItemFragment extends NbFragment {
private DefaultFeedView selectedFeedView; private DefaultFeedView selectedFeedView;
private boolean textViewUnavailable; private boolean textViewUnavailable;
@Bind(R.id.reading_textloading) TextView textViewLoadingMsg; @Bind(R.id.reading_textloading) TextView textViewLoadingMsg;
@Bind(R.id.reading_textmodefailed) TextView textViewLoadingFailedMsg;
@Bind(R.id.save_story_button) Button saveButton; @Bind(R.id.save_story_button) Button saveButton;
@Bind(R.id.share_story_button) Button shareButton; @Bind(R.id.share_story_button) Button shareButton;
@Bind(R.id.story_context_menu_button) Button menuButton;
/** The story HTML, as provided by the 'content' element of the stories API. */ /** The story HTML, as provided by the 'content' element of the stories API. */
private String storyContent; private String storyContent;
@ -102,6 +107,7 @@ public class ReadingItemFragment extends NbFragment {
private boolean isWebLoadFinished; private boolean isWebLoadFinished;
private boolean isSocialLoadFinished; private boolean isSocialLoadFinished;
private Boolean isLoadFinished = false; private Boolean isLoadFinished = false;
private float savedScrollPosRel = 0f;
private final Object WEBVIEW_CONTENT_MUTEX = new Object(); private final Object WEBVIEW_CONTENT_MUTEX = new Object();
@ -129,8 +135,6 @@ public class ReadingItemFragment extends NbFragment {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
story = getArguments() != null ? (Story) getArguments().getSerializable("story") : null; story = getArguments() != null ? (Story) getArguments().getSerializable("story") : null;
inflater = getActivity().getLayoutInflater();
displayFeedDetails = getArguments().getBoolean("displayFeedDetails"); displayFeedDetails = getArguments().getBoolean("displayFeedDetails");
user = PrefsUtils.getUserDetails(getActivity()); user = PrefsUtils.getUserDetails(getActivity());
@ -150,11 +154,19 @@ public class ReadingItemFragment extends NbFragment {
getActivity().registerReceiver(textSizeReceiver, new IntentFilter(TEXT_SIZE_CHANGED)); getActivity().registerReceiver(textSizeReceiver, new IntentFilter(TEXT_SIZE_CHANGED));
readingFontReceiver = new ReadingFontReceiver(); readingFontReceiver = new ReadingFontReceiver();
getActivity().registerReceiver(readingFontReceiver, new IntentFilter(READING_FONT_CHANGED)); getActivity().registerReceiver(readingFontReceiver, new IntentFilter(READING_FONT_CHANGED));
if (savedInstanceState != null) {
savedScrollPosRel = savedInstanceState.getFloat(BUNDLE_SCROLL_POS_REL);
// we can't actually use the saved scroll position until the webview finishes loading
}
} }
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
outState.putSerializable("story", story); super.onSaveInstanceState(outState);
int heightm = fragmentScrollview.getChildAt(0).getMeasuredHeight();
int pos = fragmentScrollview.getScrollY();
outState.putFloat(BUNDLE_SCROLL_POS_REL, (((float)pos)/heightm));
} }
@Override @Override
@ -182,7 +194,8 @@ public class ReadingItemFragment extends NbFragment {
if (this.web != null ) { this.web.onResume(); } if (this.web != null ) { this.web.onResume(); }
} }
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
this.inflater = inflater;
view = inflater.inflate(R.layout.fragment_readingitem, null); view = inflater.inflate(R.layout.fragment_readingitem, null);
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
@ -274,6 +287,90 @@ public class ReadingItemFragment extends NbFragment {
} }
} }
@OnClick(R.id.story_context_menu_button) void onClickMenuButton() {
PopupMenu pm = new PopupMenu(getActivity(), menuButton);
Menu menu = pm.getMenu();
pm.getMenuInflater().inflate(R.menu.story_context, menu);
menu.findItem(R.id.menu_reading_save).setTitle(story.starred ? R.string.menu_unsave_story : R.string.menu_save_story);
if (fs.isFilterSaved() || fs.isAllSaved() || (fs.getSingleSavedTag() != null)) menu.findItem(R.id.menu_reading_markunread).setVisible(false);
ThemeValue themeValue = PrefsUtils.getSelectedTheme(getActivity());
if (themeValue == ThemeValue.LIGHT) {
menu.findItem(R.id.menu_theme_light).setChecked(true);
} else if (themeValue == ThemeValue.DARK) {
menu.findItem(R.id.menu_theme_dark).setChecked(true);
} else if (themeValue == ThemeValue.BLACK) {
menu.findItem(R.id.menu_theme_black).setChecked(true);
}
pm.setOnMenuItemClickListener(this);
pm.show();
}
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.menu_reading_original) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(story.permalink));
try {
startActivity(i);
} catch (Exception e) {
com.newsblur.util.Log.e(this, "device cannot open URLs");
}
return true;
} else if (item.getItemId() == R.id.menu_reading_sharenewsblur) {
String sourceUserId = null;
if (fs.getSingleSocialFeed() != null) sourceUserId = fs.getSingleSocialFeed().getKey();
DialogFragment newFragment = ShareDialogFragment.newInstance(story, sourceUserId);
newFragment.show(getActivity().getSupportFragmentManager(), "dialog");
return true;
} else if (item.getItemId() == R.id.menu_send_story) {
FeedUtils.sendStoryBrief(story, getActivity());
return true;
} else if (item.getItemId() == R.id.menu_send_story_full) {
FeedUtils.sendStoryFull(story, getActivity());
return true;
} else if (item.getItemId() == R.id.menu_textsize) {
TextSizeDialogFragment textSize = TextSizeDialogFragment.newInstance(PrefsUtils.getTextSize(getActivity()), TextSizeDialogFragment.TextSizeType.ReadingText);
textSize.show(getActivity().getSupportFragmentManager(), TextSizeDialogFragment.class.getName());
return true;
} else if (item.getItemId() == R.id.menu_font) {
ReadingFontDialogFragment storyFont = ReadingFontDialogFragment.newInstance(PrefsUtils.getFontString(getActivity()));
storyFont.show(getActivity().getSupportFragmentManager(), ReadingFontDialogFragment.class.getName());
return true;
} else if (item.getItemId() == R.id.menu_reading_save) {
if (story.starred) {
FeedUtils.setStorySaved(story, false, getActivity());
} else {
FeedUtils.setStorySaved(story, true, getActivity());
}
return true;
} else if (item.getItemId() == R.id.menu_reading_markunread) {
FeedUtils.markStoryUnread(story, getActivity());
return true;
} else if (item.getItemId() == R.id.menu_theme_light) {
PrefsUtils.setSelectedTheme(getActivity(), ThemeValue.LIGHT);
UIUtils.restartActivity(getActivity());
return true;
} else if (item.getItemId() == R.id.menu_theme_dark) {
PrefsUtils.setSelectedTheme(getActivity(), ThemeValue.DARK);
UIUtils.restartActivity(getActivity());
return true;
} else if (item.getItemId() == R.id.menu_theme_black) {
PrefsUtils.setSelectedTheme(getActivity(), ThemeValue.BLACK);
UIUtils.restartActivity(getActivity());
return true;
} else if (item.getItemId() == R.id.menu_intel) {
if (story.feedId.equals("0")) return true; // cannot train on feedless stories
StoryIntelTrainerFragment intelFrag = StoryIntelTrainerFragment.newInstance(story, fs);
intelFrag.show(getActivity().getSupportFragmentManager(), StoryIntelTrainerFragment.class.getName());
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
@OnClick(R.id.save_story_button) void clickSave() { @OnClick(R.id.save_story_button) void clickSave() {
if (story.starred) { if (story.starred) {
FeedUtils.setStorySaved(story, false, getActivity()); FeedUtils.setStorySaved(story, false, getActivity());
@ -500,25 +597,38 @@ public class ReadingItemFragment extends NbFragment {
} }
private void reloadStoryContent() { private void reloadStoryContent() {
if ((selectedFeedView == DefaultFeedView.STORY) || textViewUnavailable) { // reset indicators
textViewLoadingMsg.setVisibility(View.GONE); textViewLoadingMsg.setVisibility(View.GONE);
enableProgress(false); textViewLoadingFailedMsg.setVisibility(View.GONE);
enableProgress(false);
boolean needStoryContent = false;
if (selectedFeedView == DefaultFeedView.STORY) {
needStoryContent = true;
} else {
if (textViewUnavailable) {
textViewLoadingFailedMsg.setVisibility(View.VISIBLE);
needStoryContent = true;
} else if (originalText == null) {
textViewLoadingMsg.setVisibility(View.VISIBLE);
enableProgress(true);
loadOriginalText();
// still show the story mode version, as the text mode one may take some time
needStoryContent = true;
} else {
setupWebview(originalText);
onContentLoadFinished();
}
}
if (needStoryContent) {
if (storyContent == null) { if (storyContent == null) {
loadStoryContent(); loadStoryContent();
} else { } else {
setupWebview(storyContent); setupWebview(storyContent);
onContentLoadFinished(); onContentLoadFinished();
} }
} else {
if (originalText == null) {
enableProgress(true);
loadOriginalText();
} else {
textViewLoadingMsg.setVisibility(View.GONE);
setupWebview(originalText);
onContentLoadFinished();
enableProgress(false);
}
} }
} }
@ -540,7 +650,7 @@ public class ReadingItemFragment extends NbFragment {
return; return;
} }
this.story = story; this.story = story;
if (AppConstants.VERBOSE_LOG) com.newsblur.util.Log.d(this, "got fresh story"); //if (AppConstants.VERBOSE_LOG) com.newsblur.util.Log.d(this, "got fresh story");
} }
public void handleUpdate(int updateType) { public void handleUpdate(int updateType) {
@ -575,7 +685,6 @@ public class ReadingItemFragment extends NbFragment {
if (OriginalTextService.NULL_STORY_TEXT.equals(result)) { if (OriginalTextService.NULL_STORY_TEXT.equals(result)) {
// the server reported that text mode is not available. kick back to story mode // the server reported that text mode is not available. kick back to story mode
com.newsblur.util.Log.d(this, "orig text not avail for story: " + story.storyHash); com.newsblur.util.Log.d(this, "orig text not avail for story: " + story.storyHash);
UIUtils.safeToast(getActivity(), R.string.text_mode_unavailable, Toast.LENGTH_SHORT);
textViewUnavailable = true; textViewUnavailable = true;
} else { } else {
ReadingItemFragment.this.originalText = result; ReadingItemFragment.this.originalText = result;
@ -583,7 +692,6 @@ public class ReadingItemFragment extends NbFragment {
reloadStoryContent(); reloadStoryContent();
} else { } else {
com.newsblur.util.Log.d(this, "orig text not yet cached for story: " + story.storyHash); com.newsblur.util.Log.d(this, "orig text not yet cached for story: " + story.storyHash);
textViewLoadingMsg.setVisibility(View.VISIBLE);
OriginalTextService.addPriorityHash(story.storyHash); OriginalTextService.addPriorityHash(story.storyHash);
triggerSync(); triggerSync();
} }
@ -746,8 +854,33 @@ public class ReadingItemFragment extends NbFragment {
} }
} }
/**
* A hook for performing actions that need to happen after all of the view has loaded, including
* the story's HTML content, all metadata views, and all associated social views.
*/
private void onLoadFinished() { private void onLoadFinished() {
// TODO: perform any position-dependent UI behaviours here (@manderson23) // if there was a scroll position saved, restore it
if (savedScrollPosRel > 0f) {
// ScrollViews containing WebViews are very particular about call timing. since the inner view
// height can drastically change as viewport width changes, position has to be saved and restored
// as a proportion of total inner view height. that height won't be known until all the various
// async bits of the fragment have finished loading. however, even after the WebView calls back
// onProgressChanged with a value of 100, immediate calls to get the size of the view will return
// incorrect values. even posting a runnable to the very end of our UI event queue may be
// insufficient time to allow the WebView to actually finish internally computing state and size.
// an additional fixed delay is added in a last ditch attempt to give the black-box platform
// threads a chance to finish their work.
fragmentScrollview.postDelayed(new Runnable() {
public void run() {
int relPos = Math.round(fragmentScrollview.getChildAt(0).getMeasuredHeight() * savedScrollPosRel);
fragmentScrollview.scrollTo(0, relPos);
}
}, 75L);
}
}
public void flagWebviewError() {
// TODO: enable a selective reload mechanism on load failures?
} }
private class TextSizeReceiver extends BroadcastReceiver { private class TextSizeReceiver extends BroadcastReceiver {

View file

@ -0,0 +1,46 @@
package com.newsblur.fragment;
import android.os.Bundle;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import butterknife.ButterKnife;
import butterknife.Bind;
import com.newsblur.R;
import com.newsblur.activity.Reading;
/*
* A fragment to hold the story pager. Eventually this fragment should hold much of the UI and logic
* currently implemented in the Reading activity. The crucial part, though, is that the pager exists
* in a wrapper fragment and that the pager is passed a *child* FragmentManager of this fragment and
* not just the standard support FM from the activity/context. The pager platform code appears to
* expect this design.
*/
public class ReadingPagerFragment extends NbFragment {
@Bind(R.id.reading_pager) ViewPager pager;
public static ReadingPagerFragment newInstance() {
ReadingPagerFragment fragment = new ReadingPagerFragment();
Bundle arguments = new Bundle();
fragment.setArguments(arguments);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_readingpager, null);
ButterKnife.bind(this, v);
Reading activity = ((Reading) getActivity());
pager.addOnPageChangeListener(activity);
activity.offerPager(pager, getChildFragmentManager());
return v;
}
}

View file

@ -0,0 +1,69 @@
package com.newsblur.fragment;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import butterknife.ButterKnife;
import butterknife.Bind;
import com.newsblur.R;
import com.newsblur.domain.Feed;
import com.newsblur.util.FeedUtils;
public class RenameFeedFragment extends DialogFragment {
private Feed feed;
@Bind(R.id.feed_name_field) EditText feedNameView;
public static RenameFeedFragment newInstance(Feed feed) {
RenameFeedFragment fragment = new RenameFeedFragment();
Bundle args = new Bundle();
args.putSerializable("feed", feed);
fragment.setArguments(args);
return fragment;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
feed = (Feed) getArguments().getSerializable("feed");
final Activity activity = getActivity();
LayoutInflater inflater = LayoutInflater.from(activity);
View v = inflater.inflate(R.layout.dialog_rename_feed, null);
ButterKnife.bind(this, v);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(String.format(getResources().getString(R.string.title_rename_feed), feed.title));
builder.setView(v);
feedNameView.setText(feed.title);
builder.setNegativeButton(R.string.alert_dialog_cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
RenameFeedFragment.this.dismiss();
}
});
builder.setPositiveButton(R.string.feed_name_save, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
FeedUtils.renameFeed(activity, feed.feedId, feedNameView.getText().toString());
RenameFeedFragment.this.dismiss();
}
});
Dialog dialog = builder.create();
return dialog;
}
}

View file

@ -70,6 +70,7 @@ public class APIConstants {
public static final String PATH_CONNECT_TWITTER = "/oauth/twitter_connect/"; public static final String PATH_CONNECT_TWITTER = "/oauth/twitter_connect/";
public static final String PATH_SET_NOTIFICATIONS = "/notifications/feed/"; public static final String PATH_SET_NOTIFICATIONS = "/notifications/feed/";
public static final String PATH_INSTA_FETCH = "/rss_feeds/exception_retry"; public static final String PATH_INSTA_FETCH = "/rss_feeds/exception_retry";
public static final String PATH_RENAME_FEED = "/reader/rename_feed";
public static String buildUrl(String path) { public static String buildUrl(String path) {
return CurrentUrlBase + path; return CurrentUrlBase + path;
@ -118,6 +119,7 @@ public class APIConstants {
public static final String PARAMETER_NOTIFICATION_FILTER = "notification_filter"; public static final String PARAMETER_NOTIFICATION_FILTER = "notification_filter";
public static final String PARAMETER_RESET_FETCH = "reset_fetch"; public static final String PARAMETER_RESET_FETCH = "reset_fetch";
public static final String PARAMETER_INFREQUENT = "infrequent"; public static final String PARAMETER_INFREQUENT = "infrequent";
public static final String PARAMETER_FEEDTITLE = "feed_title";
public static final String VALUE_PREFIX_SOCIAL = "social:"; 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 public static final String VALUE_ALLSOCIAL = "river:blurblogs"; // the magic value passed to the mark-read API for all social feeds

View file

@ -599,6 +599,14 @@ public class APIManager {
return response.getResponse(gson, NewsBlurResponse.class); return response.getResponse(gson, NewsBlurResponse.class);
} }
public NewsBlurResponse renameFeed(String feedId, String newFeedName) {
ValueMultimap values = new ValueMultimap();
values.put(APIConstants.PARAMETER_FEEDID, feedId);
values.put(APIConstants.PARAMETER_FEEDTITLE, newFeedName);
APIResponse response = post(buildUrl(APIConstants.PATH_RENAME_FEED), values);
return response.getResponse(gson, NewsBlurResponse.class);
}
/* HTTP METHODS */ /* HTTP METHODS */
private APIResponse get(final String urlString) { private APIResponse get(final String urlString) {

View file

@ -1,13 +1,14 @@
package com.newsblur.service; package com.newsblur.service;
import android.app.AlarmManager; import android.app.job.JobInfo;
import android.app.PendingIntent; import android.app.job.JobScheduler;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.util.Log;
import com.newsblur.util.AppConstants; import com.newsblur.util.AppConstants;
import com.newsblur.service.NBSyncService;
/** /**
* First receiver in the chain that starts with the device. Simply schedules another broadcast * First receiver in the chain that starts with the device. Simply schedules another broadcast
@ -17,20 +18,18 @@ public class BootReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
Log.d(this.getClass().getName(), "triggering sync service from device boot"); com.newsblur.util.Log.d(this, "triggering sync service from device boot");
scheduleSyncService(context); scheduleSyncService(context);
} }
public static void scheduleSyncService(Context context) { public static void scheduleSyncService(Context context) {
Log.d(BootReceiver.class.getName(), "scheduling sync service"); com.newsblur.util.Log.d(BootReceiver.class.getName(), "scheduling sync service");
JobInfo.Builder builder = new JobInfo.Builder(1, new ComponentName(context, NBSyncService.class));
// wake up to check if a sync is needed less often than necessary to compensate for execution time builder.setPeriodic(AppConstants.BG_SERVICE_CYCLE_MILLIS);
long interval = AppConstants.BG_SERVICE_CYCLE_MILLIS; builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY);
builder.setPersisted(true);
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); JobScheduler sched = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
Intent i = new Intent(context, ServiceScheduleReceiver.class); sched.schedule(builder.build());
PendingIntent pi = PendingIntent.getBroadcast(context, 0, i, PendingIntent.FLAG_CANCEL_CURRENT);
alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, interval, interval, pi);
} }
} }

View file

@ -6,7 +6,7 @@ import com.newsblur.util.PrefsUtils;
public class CleanupService extends SubService { public class CleanupService extends SubService {
private static volatile boolean Running = false; public static boolean activelyRunning = false;
public CleanupService(NBSyncService parent) { public CleanupService(NBSyncService parent) {
super(parent); super(parent);
@ -14,16 +14,17 @@ public class CleanupService extends SubService {
@Override @Override
protected void exec() { protected void exec() {
gotWork();
if (PrefsUtils.isTimeToCleanup(parent)) { if (!PrefsUtils.isTimeToCleanup(parent)) return;
com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up old stories");
parent.dbHelper.cleanupVeryOldStories(); activelyRunning = true;
if (!PrefsUtils.isKeepOldStories(parent)) {
parent.dbHelper.cleanupReadStories(); com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up old stories");
} parent.dbHelper.cleanupVeryOldStories();
PrefsUtils.updateLastCleanupTime(parent); if (!PrefsUtils.isKeepOldStories(parent)) {
parent.dbHelper.cleanupReadStories();
} }
PrefsUtils.updateLastCleanupTime(parent);
com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up old story texts"); com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up old story texts");
parent.dbHelper.cleanupStoryText(); parent.dbHelper.cleanupStoryText();
@ -42,19 +43,9 @@ public class CleanupService extends SubService {
com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up thumbnail cache"); com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up thumbnail cache");
FileCache thumbCache = FileCache.asThumbnailCache(parent); FileCache thumbCache = FileCache.asThumbnailCache(parent);
thumbCache.cleanupUnusedAndOld(parent.dbHelper.getAllStoryThumbnails(), PrefsUtils.getMaxCachedAgeMillis(parent)); thumbCache.cleanupUnusedAndOld(parent.dbHelper.getAllStoryThumbnails(), PrefsUtils.getMaxCachedAgeMillis(parent));
}
public static boolean running() { activelyRunning = false;
return Running;
} }
@Override
protected void setRunning(boolean running) {
Running = running;
}
@Override
protected boolean isRunning() {
return Running;
}
} }

View file

@ -12,7 +12,7 @@ import java.util.Set;
public class ImagePrefetchService extends SubService { public class ImagePrefetchService extends SubService {
private static volatile boolean Running = false; public static boolean activelyRunning = false;
FileCache storyImageCache; FileCache storyImageCache;
FileCache thumbnailCache; FileCache thumbnailCache;
@ -33,77 +33,77 @@ public class ImagePrefetchService extends SubService {
@Override @Override
protected void exec() { protected void exec() {
if (!PrefsUtils.isImagePrefetchEnabled(parent)) return; activelyRunning = true;
if (!PrefsUtils.isBackgroundNetworkAllowed(parent)) return; try {
if (!PrefsUtils.isImagePrefetchEnabled(parent)) return;
if (!PrefsUtils.isBackgroundNetworkAllowed(parent)) return;
gotWork(); while (StoryImageQueue.size() > 0) {
if (! PrefsUtils.isImagePrefetchEnabled(parent)) return;
if (! PrefsUtils.isBackgroundNetworkAllowed(parent)) return;
while (StoryImageQueue.size() > 0) { startExpensiveCycle();
if (! PrefsUtils.isImagePrefetchEnabled(parent)) return; com.newsblur.util.Log.d(this, "story images to prefetch: " + StoryImageQueue.size());
if (! PrefsUtils.isBackgroundNetworkAllowed(parent)) return; // on each batch, re-query the DB for images associated with yet-unread stories
// this is a bit expensive, but we are running totally async at a really low priority
startExpensiveCycle(); Set<String> unreadImages = parent.dbHelper.getAllStoryImages();
com.newsblur.util.Log.d(this, "story images to prefetch: " + StoryImageQueue.size()); Set<String> fetchedImages = new HashSet<String>();
// on each batch, re-query the DB for images associated with yet-unread stories Set<String> batch = new HashSet<String>(AppConstants.IMAGE_PREFETCH_BATCH_SIZE);
// this is a bit expensive, but we are running totally async at a really low priority batchloop: for (String url : StoryImageQueue) {
Set<String> unreadImages = parent.dbHelper.getAllStoryImages(); batch.add(url);
Set<String> fetchedImages = new HashSet<String>(); if (batch.size() >= AppConstants.IMAGE_PREFETCH_BATCH_SIZE) break batchloop;
Set<String> batch = new HashSet<String>(AppConstants.IMAGE_PREFETCH_BATCH_SIZE);
batchloop: for (String url : StoryImageQueue) {
batch.add(url);
if (batch.size() >= AppConstants.IMAGE_PREFETCH_BATCH_SIZE) break batchloop;
}
try {
fetchloop: for (String url : batch) {
if (parent.stopSync()) break fetchloop;
// dont fetch the image if the associated story was marked read before we got to it
if (unreadImages.contains(url)) {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "prefetching image: " + url);
storyImageCache.cacheFile(url);
}
fetchedImages.add(url);
} }
} finally { try {
StoryImageQueue.removeAll(fetchedImages); fetchloop: for (String url : batch) {
com.newsblur.util.Log.d(this, "story images fetched: " + fetchedImages.size()); if (parent.stopSync()) break fetchloop;
gotWork(); // dont fetch the image if the associated story was marked read before we got to it
} if (unreadImages.contains(url)) {
} if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "prefetching image: " + url);
storyImageCache.cacheFile(url);
if (parent.stopSync()) return; }
fetchedImages.add(url);
while (ThumbnailQueue.size() > 0) {
if (! PrefsUtils.isImagePrefetchEnabled(parent)) return;
if (! PrefsUtils.isBackgroundNetworkAllowed(parent)) return;
startExpensiveCycle();
com.newsblur.util.Log.d(this, "story thumbs to prefetch: " + StoryImageQueue.size());
// on each batch, re-query the DB for images associated with yet-unread stories
// this is a bit expensive, but we are running totally async at a really low priority
Set<String> unreadImages = parent.dbHelper.getAllStoryThumbnails();
Set<String> fetchedImages = new HashSet<String>();
Set<String> batch = new HashSet<String>(AppConstants.IMAGE_PREFETCH_BATCH_SIZE);
batchloop: for (String url : ThumbnailQueue) {
batch.add(url);
if (batch.size() >= AppConstants.IMAGE_PREFETCH_BATCH_SIZE) break batchloop;
}
try {
fetchloop: for (String url : batch) {
if (parent.stopSync()) break fetchloop;
// dont fetch the image if the associated story was marked read before we got to it
if (unreadImages.contains(url)) {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "prefetching thumbnail: " + url);
thumbnailCache.cacheFile(url);
} }
fetchedImages.add(url); } finally {
StoryImageQueue.removeAll(fetchedImages);
com.newsblur.util.Log.d(this, "story images fetched: " + fetchedImages.size());
} }
} finally {
ThumbnailQueue.removeAll(fetchedImages);
com.newsblur.util.Log.d(this, "story thumbs fetched: " + fetchedImages.size());
gotWork();
} }
if (parent.stopSync()) return;
while (ThumbnailQueue.size() > 0) {
if (! PrefsUtils.isImagePrefetchEnabled(parent)) return;
if (! PrefsUtils.isBackgroundNetworkAllowed(parent)) return;
startExpensiveCycle();
com.newsblur.util.Log.d(this, "story thumbs to prefetch: " + StoryImageQueue.size());
// on each batch, re-query the DB for images associated with yet-unread stories
// this is a bit expensive, but we are running totally async at a really low priority
Set<String> unreadImages = parent.dbHelper.getAllStoryThumbnails();
Set<String> fetchedImages = new HashSet<String>();
Set<String> batch = new HashSet<String>(AppConstants.IMAGE_PREFETCH_BATCH_SIZE);
batchloop: for (String url : ThumbnailQueue) {
batch.add(url);
if (batch.size() >= AppConstants.IMAGE_PREFETCH_BATCH_SIZE) break batchloop;
}
try {
fetchloop: for (String url : batch) {
if (parent.stopSync()) break fetchloop;
// dont fetch the image if the associated story was marked read before we got to it
if (unreadImages.contains(url)) {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "prefetching thumbnail: " + url);
thumbnailCache.cacheFile(url);
}
fetchedImages.add(url);
}
} finally {
ThumbnailQueue.removeAll(fetchedImages);
com.newsblur.util.Log.d(this, "story thumbs fetched: " + fetchedImages.size());
}
}
} finally {
activelyRunning = false;
} }
} }
public void addUrl(String url) { public void addUrl(String url) {
@ -118,27 +118,10 @@ public class ImagePrefetchService extends SubService {
return (StoryImageQueue.size() + ThumbnailQueue.size()); return (StoryImageQueue.size() + ThumbnailQueue.size());
} }
@Override
public boolean haveWork() {
return (getPendingCount() > 0);
}
public static void clear() { public static void clear() {
StoryImageQueue.clear(); StoryImageQueue.clear();
ThumbnailQueue.clear(); ThumbnailQueue.clear();
} }
public static boolean running() {
return Running;
}
@Override
protected void setRunning(boolean running) {
Running = running;
}
@Override
protected boolean isRunning() {
return Running;
}
} }

View file

@ -1,15 +1,13 @@
package com.newsblur.service; package com.newsblur.service;
import android.app.Service; import android.app.Service;
import android.content.ComponentCallbacks2; import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.Process; import android.os.Process;
import android.util.Log;
import com.newsblur.R; import com.newsblur.R;
import com.newsblur.activity.NbActivity; import com.newsblur.activity.NbActivity;
@ -31,6 +29,7 @@ import com.newsblur.util.AppConstants;
import com.newsblur.util.DefaultFeedView; import com.newsblur.util.DefaultFeedView;
import com.newsblur.util.FeedSet; import com.newsblur.util.FeedSet;
import com.newsblur.util.FileCache; import com.newsblur.util.FileCache;
import com.newsblur.util.Log;
import com.newsblur.util.NetworkUtils; import com.newsblur.util.NetworkUtils;
import com.newsblur.util.NotificationUtils; import com.newsblur.util.NotificationUtils;
import com.newsblur.util.PrefsUtils; import com.newsblur.util.PrefsUtils;
@ -66,9 +65,9 @@ import java.util.concurrent.TimeUnit;
* after sync operations are performed. Activities can then refresh views and * after sync operations are performed. Activities can then refresh views and
* query this class to see if progress indicators should be active. * query this class to see if progress indicators should be active.
*/ */
public class NBSyncService extends Service { public class NBSyncService extends JobService {
private static final Object WAKELOCK_MUTEX = new Object(); private static final Object COMPLETION_CALLBACKS_MUTEX = new Object();
private static final Object PENDING_FEED_MUTEX = new Object(); private static final Object PENDING_FEED_MUTEX = new Object();
private volatile static boolean ActionsRunning = false; private volatile static boolean ActionsRunning = false;
@ -88,7 +87,6 @@ public class NBSyncService extends Service {
public volatile static Boolean isPremium = null; public volatile static Boolean isPremium = null;
public volatile static Boolean isStaff = null; public volatile static Boolean isStaff = null;
private volatile static boolean isMemoryLow = false;
private static long lastFeedCount = 0L; private static long lastFeedCount = 0L;
private static long lastFFConnMillis = 0L; private static long lastFFConnMillis = 0L;
private static long lastFFReadMillis = 0L; private static long lastFFReadMillis = 0L;
@ -130,16 +128,18 @@ public class NBSyncService extends Service {
Set<String> disabledFeedIds = new HashSet<String>(); Set<String> disabledFeedIds = new HashSet<String>();
private ExecutorService primaryExecutor; private ExecutorService primaryExecutor;
private List<Integer> outstandingStartIds = new ArrayList<Integer>();
private List<JobParameters> outstandingStartParams = new ArrayList<JobParameters>();
private boolean mainSyncRunning = false;
CleanupService cleanupService; CleanupService cleanupService;
OriginalTextService originalTextService; OriginalTextService originalTextService;
UnreadsService unreadsService; UnreadsService unreadsService;
ImagePrefetchService imagePrefetchService; ImagePrefetchService imagePrefetchService;
private boolean forceHalted = false;
PowerManager.WakeLock wl = null;
APIManager apiManager; APIManager apiManager;
BlurDatabaseHelper dbHelper; BlurDatabaseHelper dbHelper;
FileCache iconCache; FileCache iconCache;
private int lastStartIdCompleted = -1;
/** The time of the last hard API failure we encountered. Used to implement back-off so that the sync /** The time of the last hard API failure we encountered. Used to implement back-off so that the sync
service doesn't spin in the background chewing up battery when the API is unavailable. */ service doesn't spin in the background chewing up battery when the API is unavailable. */
@ -152,10 +152,6 @@ public class NBSyncService extends Service {
super.onCreate(); super.onCreate();
com.newsblur.util.Log.d(this, "onCreate"); com.newsblur.util.Log.d(this, "onCreate");
HaltNow = false; HaltNow = false;
PowerManager pm = (PowerManager) getApplicationContext().getSystemService(POWER_SERVICE);
wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.getClass().getSimpleName());
wl.setReferenceCounted(true);
primaryExecutor = Executors.newFixedThreadPool(1); primaryExecutor = Executors.newFixedThreadPool(1);
} }
@ -164,7 +160,7 @@ public class NBSyncService extends Service {
* parts of construction in onCreate, but save them for when we are in our own thread. * parts of construction in onCreate, but save them for when we are in our own thread.
*/ */
private void finishConstruction() { private void finishConstruction() {
if (apiManager == null) { if ((apiManager == null) || (dbHelper == null)) {
apiManager = new APIManager(this); apiManager = new APIManager(this);
dbHelper = new BlurDatabaseHelper(this); dbHelper = new BlurDatabaseHelper(this);
iconCache = FileCache.asIconCache(this); iconCache = FileCache.asIconCache(this);
@ -177,42 +173,89 @@ public class NBSyncService extends Service {
} }
/** /**
* Called serially, once per "start" of the service. This serves as a wakeup call * Kickoff hook for when we are started via Context.startService()
* that the service should check for outstanding work.
*/ */
@Override @Override
public int onStartCommand(Intent intent, int flags, final int startId) { public int onStartCommand(Intent intent, int flags, final int startId) {
com.newsblur.util.Log.d(this, "onStartCommand");
// only perform a sync if the app is actually running or background syncs are enabled // only perform a sync if the app is actually running or background syncs are enabled
if ((NbActivity.getActiveActivityCount() > 0) || PrefsUtils.isBackgroundNeeded(this)) { if ((NbActivity.getActiveActivityCount() > 0) || PrefsUtils.isBackgroundNeeded(this)) {
HaltNow = false;
// Services actually get invoked on the main system thread, and are not // Services actually get invoked on the main system thread, and are not
// allowed to do tangible work. We spawn a thread to do so. // allowed to do tangible work. We spawn a thread to do so.
Runnable r = new Runnable() { Runnable r = new Runnable() {
public void run() { public void run() {
doSync(startId); mainSyncRunning = true;
doSync();
mainSyncRunning = false;
// record the startId so when the sync thread and all sub-service threads finish,
// we can report that this invocation completed.
synchronized (COMPLETION_CALLBACKS_MUTEX) {outstandingStartIds.add(startId);}
checkCompletion();
} }
}; };
primaryExecutor.execute(r); primaryExecutor.execute(r);
} else { } else {
com.newsblur.util.Log.d(this, "Skipping sync: app not active and background sync not enabled."); com.newsblur.util.Log.i(this, "Skipping sync: app not active and background sync not enabled.");
stopSelf(startId); synchronized (COMPLETION_CALLBACKS_MUTEX) {outstandingStartIds.add(startId);}
checkCompletion();
} }
// indicate to the system that the service should be alive when started, but // indicate to the system that the service should be alive when started, but
// needn't necessarily persist under memory pressure // needn't necessarily persist under memory pressure
return Service.START_NOT_STICKY; return Service.START_NOT_STICKY;
} }
/**
* Kickoff hook for when we are started via a JobScheduler
*/
@Override
public boolean onStartJob(final JobParameters params) {
com.newsblur.util.Log.d(this, "onStartJob");
// only perform a sync if the app is actually running or background syncs are enabled
if ((NbActivity.getActiveActivityCount() > 0) || PrefsUtils.isBackgroundNeeded(this)) {
HaltNow = false;
// Services actually get invoked on the main system thread, and are not
// allowed to do tangible work. We spawn a thread to do so.
Runnable r = new Runnable() {
public void run() {
mainSyncRunning = true;
doSync();
mainSyncRunning = false;
// record the JobParams so when the sync thread and all sub-service threads finish,
// we can report that this invocation completed.
synchronized (COMPLETION_CALLBACKS_MUTEX) {outstandingStartParams.add(params);}
checkCompletion();
}
};
primaryExecutor.execute(r);
} else {
com.newsblur.util.Log.d(this, "Skipping sync: app not active and background sync not enabled.");
synchronized (COMPLETION_CALLBACKS_MUTEX) {outstandingStartParams.add(params);}
checkCompletion();
}
return true; // indicate that we are async
}
@Override
public boolean onStopJob(JobParameters params) {
com.newsblur.util.Log.d(this, "onStopJob");
HaltNow = true;
// return false to indicate that we don't necessarily need re-invocation ahead of schedule.
// background syncs can pick up where the last one left off and forground syncs aren't
// run via cancellable JobScheduler invocations.
return false;
}
/** /**
* Do the actual work of syncing. * Do the actual work of syncing.
*/ */
private synchronized void doSync(final int startId) { private synchronized void doSync() {
try { try {
if (HaltNow) return; if (HaltNow) return;
incrementRunningChild();
finishConstruction(); finishConstruction();
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "starting primary sync"); Log.d(this, "starting primary sync");
if (NbActivity.getActiveActivityCount() < 1) { if (NbActivity.getActiveActivityCount() < 1) {
// if the UI isn't running, politely run at background priority // if the UI isn't running, politely run at background priority
@ -250,16 +293,16 @@ public class NBSyncService extends Service {
NbActivity.updateAllActivities(NbActivity.UPDATE_DB_READY); NbActivity.updateAllActivities(NbActivity.UPDATE_DB_READY);
// async text requests might have been queued up and are being waiting on by the live UI. give them priority // async text requests might have been queued up and are being waiting on by the live UI. give them priority
originalTextService.start(startId); originalTextService.start();
// first: catch up // first: catch up
syncActions(); syncActions();
// if MD is stale, sync it first so unreads don't get backwards with story unread state // if MD is stale, sync it first so unreads don't get backwards with story unread state
syncMetadata(startId); syncMetadata();
// handle fetching of stories that are actively being requested by the live UI // handle fetching of stories that are actively being requested by the live UI
syncPendingFeedStories(startId); syncPendingFeedStories();
// re-apply the local state of any actions executed before local UI interaction // re-apply the local state of any actions executed before local UI interaction
finishActions(); finishActions();
@ -268,20 +311,18 @@ public class NBSyncService extends Service {
checkRecounts(); checkRecounts();
// async story and image prefetch are lower priority and don't affect active reading, do them last // async story and image prefetch are lower priority and don't affect active reading, do them last
unreadsService.start(startId); unreadsService.start();
imagePrefetchService.start(startId); imagePrefetchService.start();
// almost all notifications will be pushed after the unreadsService gets new stories, but double-check // almost all notifications will be pushed after the unreadsService gets new stories, but double-check
// here in case some made it through the feed sync loop first // here in case some made it through the feed sync loop first
pushNotifications(); pushNotifications();
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "finishing primary sync"); Log.d(this, "finishing primary sync");
} catch (Exception e) { } catch (Exception e) {
com.newsblur.util.Log.e(this.getClass().getName(), "Sync error.", e); com.newsblur.util.Log.e(this.getClass().getName(), "Sync error.", e);
} finally { }
decrementRunningChild(startId);
}
} }
/** /**
@ -397,7 +438,7 @@ public class NBSyncService extends Service {
if (HaltNow) return; if (HaltNow) return;
if (FollowupActions.size() < 1) return; if (FollowupActions.size() < 1) return;
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "double-checking " + FollowupActions.size() + " actions"); Log.d(this, "double-checking " + FollowupActions.size() + " actions");
int impactFlags = 0; int impactFlags = 0;
for (ReadingAction ra : FollowupActions) { for (ReadingAction ra : FollowupActions) {
int impact = ra.doLocal(dbHelper, true); int impact = ra.doLocal(dbHelper, true);
@ -420,7 +461,7 @@ public class NBSyncService extends Service {
* The very first step of a sync - get the feed/folder list, unread counts, and * The very first step of a sync - get the feed/folder list, unread counts, and
* unread hashes. Doing this resets pagination on the server! * unread hashes. Doing this resets pagination on the server!
*/ */
private void syncMetadata(int startId) { private void syncMetadata() {
if (stopSync()) return; if (stopSync()) return;
if (backoffBackgroundCalls()) return; if (backoffBackgroundCalls()) return;
int untriedActions = dbHelper.getUntriedActionCount(); int untriedActions = dbHelper.getUntriedActionCount();
@ -559,8 +600,8 @@ public class NBSyncService extends Service {
com.newsblur.util.Log.i(this.getClass().getName(), "got feed list: " + getSpeedInfo()); com.newsblur.util.Log.i(this.getClass().getName(), "got feed list: " + getSpeedInfo());
UnreadsService.doMetadata(); UnreadsService.doMetadata();
unreadsService.start(startId); unreadsService.start();
cleanupService.start(startId); cleanupService.start();
} finally { } finally {
FFSyncRunning = false; FFSyncRunning = false;
@ -654,7 +695,7 @@ public class NBSyncService extends Service {
/** /**
* Fetch stories needed because the user is actively viewing a feed or folder. * Fetch stories needed because the user is actively viewing a feed or folder.
*/ */
private void syncPendingFeedStories(int startId) { private void syncPendingFeedStories() {
// track whether we actually tried to handle the feedset and found we had nothing // track whether we actually tried to handle the feedset and found we had nothing
// more to do, in which case we will clear it // more to do, in which case we will clear it
boolean finished = false; boolean finished = false;
@ -679,6 +720,7 @@ public class NBSyncService extends Service {
} }
if (fs == null) { if (fs == null) {
com.newsblur.util.Log.d(this.getClass().getName(), "No feed set to sync");
return; return;
} }
@ -731,7 +773,7 @@ public class NBSyncService extends Service {
finishActions(); finishActions();
NbActivity.updateAllActivities(NbActivity.UPDATE_STORY | NbActivity.UPDATE_STATUS); NbActivity.updateAllActivities(NbActivity.UPDATE_STORY | NbActivity.UPDATE_STATUS);
prefetchOriginalText(apiResponse, startId); prefetchOriginalText(apiResponse);
FeedPagesSeen.put(fs, pageNumber); FeedPagesSeen.put(fs, pageNumber);
totalStoriesSeen += apiResponse.stories.length; totalStoriesSeen += apiResponse.stories.length;
@ -843,7 +885,7 @@ public class NBSyncService extends Service {
dbHelper.insertStories(apiResponse, false); dbHelper.insertStories(apiResponse, false);
} }
void prefetchOriginalText(StoriesResponse apiResponse, int startId) { void prefetchOriginalText(StoriesResponse apiResponse) {
storyloop: for (Story story : apiResponse.stories) { storyloop: for (Story story : apiResponse.stories) {
// only prefetch for unreads, so we don't grind to cache when the user scrolls // only prefetch for unreads, so we don't grind to cache when the user scrolls
// through old read stories // through old read stories
@ -856,10 +898,10 @@ public class NBSyncService extends Service {
} }
} }
} }
originalTextService.startConditional(startId); originalTextService.start();
} }
void prefetchImages(StoriesResponse apiResponse, int startId) { void prefetchImages(StoriesResponse apiResponse) {
storyloop: for (Story story : apiResponse.stories) { storyloop: for (Story story : apiResponse.stories) {
// only prefetch for unreads, so we don't grind to cache when the user scrolls // only prefetch for unreads, so we don't grind to cache when the user scrolls
// through old read stories // through old read stories
@ -874,7 +916,7 @@ public class NBSyncService extends Service {
imagePrefetchService.addThumbnailUrl(story.thumbnailUrl); imagePrefetchService.addThumbnailUrl(story.thumbnailUrl);
} }
} }
imagePrefetchService.startConditional(startId); imagePrefetchService.start();
} }
void pushNotifications() { void pushNotifications() {
@ -892,27 +934,28 @@ public class NBSyncService extends Service {
closeQuietly(cUnread); closeQuietly(cUnread);
} }
void incrementRunningChild() { /**
synchronized (WAKELOCK_MUTEX) { * Check to see if all async sync tasks have completed, indicating that sync can me marked as
wl.acquire(); * complete. Call this any time any individual sync task finishes.
} */
} void checkCompletion() {
//Log.d(this, "checking completion");
void decrementRunningChild(int startId) { if (mainSyncRunning) return;
synchronized (WAKELOCK_MUTEX) { if ((cleanupService != null) && cleanupService.isRunning()) return;
if (wl == null) return; if ((originalTextService != null) && originalTextService.isRunning()) return;
if (wl.isHeld()) { if ((unreadsService != null) && unreadsService.isRunning()) return;
wl.release(); if ((imagePrefetchService != null) && imagePrefetchService.isRunning()) return;
Log.d(this, "confirmed completion");
// iff all threads have finished, mark all received work as completed
synchronized (COMPLETION_CALLBACKS_MUTEX) {
for (JobParameters params : outstandingStartParams) {
jobFinished(params, forceHalted);
} }
// our wakelock reference counts. only stop the service if it is in the background and if for (Integer startId : outstandingStartIds) {
// we are the last thread to release the lock. stopSelf(startId);
if (!wl.isHeld()) {
if (NbActivity.getActiveActivityCount() < 1) {
stopSelf(startId);
}
lastStartIdCompleted = startId;
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "wakelock depleted");
} }
outstandingStartIds.clear();
outstandingStartParams.clear();
} }
} }
@ -945,18 +988,6 @@ public class NBSyncService extends Service {
return true; return true;
} }
public void onTrimMemory (int level) {
if (level > ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
isMemoryLow = true;
}
// this is also called when the UI is hidden, so double check if we need to
// stop
if ( (lastStartIdCompleted != -1) && (NbActivity.getActiveActivityCount() < 1)) {
stopSelf(lastStartIdCompleted);
}
}
/** /**
* Is the main feed/folder list sync running and blocking? * Is the main feed/folder list sync running and blocking?
*/ */
@ -994,14 +1025,14 @@ public class NBSyncService extends Service {
if (OfflineNow) return context.getResources().getString(R.string.sync_status_offline); if (OfflineNow) return context.getResources().getString(R.string.sync_status_offline);
if (HousekeepingRunning) return context.getResources().getString(R.string.sync_status_housekeeping); if (HousekeepingRunning) return context.getResources().getString(R.string.sync_status_housekeeping);
if (FFSyncRunning) return context.getResources().getString(R.string.sync_status_ffsync); if (FFSyncRunning) return context.getResources().getString(R.string.sync_status_ffsync);
if (CleanupService.running()) return context.getResources().getString(R.string.sync_status_cleanup); if (CleanupService.activelyRunning) return context.getResources().getString(R.string.sync_status_cleanup);
if (brief && !AppConstants.VERBOSE_LOG) return null; if (brief && !AppConstants.VERBOSE_LOG) return null;
if (ActionsRunning) return String.format(context.getResources().getString(R.string.sync_status_actions), lastActionCount); if (ActionsRunning) return String.format(context.getResources().getString(R.string.sync_status_actions), lastActionCount);
if (RecountsRunning) return context.getResources().getString(R.string.sync_status_recounts); if (RecountsRunning) return context.getResources().getString(R.string.sync_status_recounts);
if (StorySyncRunning) return context.getResources().getString(R.string.sync_status_stories); 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 (UnreadsService.activelyRunning) 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 (OriginalTextService.activelyRunning) 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()); if (ImagePrefetchService.activelyRunning) return String.format(context.getResources().getString(R.string.sync_status_images), ImagePrefetchService.getPendingCount());
return null; return null;
} }
@ -1047,7 +1078,7 @@ public class NBSyncService extends Service {
PendingFeed = fs; PendingFeed = fs;
PendingFeedTarget = desiredStoryCount; PendingFeedTarget = desiredStoryCount;
//if (AppConstants.VERBOSE_LOG) Log.d(NBSyncService.class.getName(), "callerhas: " + callerSeen + " have:" + alreadySeen + " want:" + desiredStoryCount + " pending:" + alreadyPending); //Log.d(NBSyncService.class.getName(), "callerhas: " + callerSeen + " have:" + alreadySeen + " want:" + desiredStoryCount + " pending:" + alreadyPending);
if (!fs.equals(LastFeedSet)) { if (!fs.equals(LastFeedSet)) {
return true; return true;
@ -1144,15 +1175,15 @@ public class NBSyncService extends Service {
ImagePrefetchService.clear(); ImagePrefetchService.clear();
} }
public static void resumeFromInterrupt() {
HaltNow = false;
}
@Override @Override
public void onDestroy() { public void onDestroy() {
try { try {
com.newsblur.util.Log.d(this, "onDestroy - stopping execution"); com.newsblur.util.Log.d(this, "onDestroy");
HaltNow = true; synchronized (COMPLETION_CALLBACKS_MUTEX) {
if ((outstandingStartIds.size() > 0) || (outstandingStartParams.size() > 0)) {
com.newsblur.util.Log.w(this, "Service scheduler destroyed before all jobs marked done?");
}
}
if (cleanupService != null) cleanupService.shutdown(); if (cleanupService != null) cleanupService.shutdown();
if (unreadsService != null) unreadsService.shutdown(); if (unreadsService != null) unreadsService.shutdown();
if (originalTextService != null) originalTextService.shutdown(); if (originalTextService != null) originalTextService.shutdown();
@ -1166,21 +1197,15 @@ public class NBSyncService extends Service {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
} }
if (dbHelper != null) dbHelper.close(); if (dbHelper != null) {
com.newsblur.util.Log.d(this, "onDestroy - execution halted"); dbHelper.close();
super.onDestroy(); dbHelper = null;
}
com.newsblur.util.Log.d(this, "onDestroy done");
} catch (Exception ex) { } catch (Exception ex) {
com.newsblur.util.Log.e(this, "unclean shutdown", ex); com.newsblur.util.Log.e(this, "unclean shutdown", ex);
} }
} super.onDestroy();
@Override
public IBinder onBind(Intent intent) {
return null;
}
public static boolean isMemoryLow() {
return isMemoryLow;
} }
public static String getSpeedInfo() { public static String getSpeedInfo() {

View file

@ -13,13 +13,13 @@ import java.util.regex.Pattern;
public class OriginalTextService extends SubService { public class OriginalTextService extends SubService {
public static boolean activelyRunning = false;
// special value for when the API responds that it could fatally could not fetch text // special value for when the API responds that it could fatally could not fetch text
public static final String NULL_STORY_TEXT = "__NULL_STORY_TEXT__"; public static final String NULL_STORY_TEXT = "__NULL_STORY_TEXT__";
private static final Pattern imgSniff = Pattern.compile("<img[^>]*src=(['\"])((?:(?!\\1).)*)\\1[^>]*>", Pattern.CASE_INSENSITIVE); private static final Pattern imgSniff = Pattern.compile("<img[^>]*src=(['\"])((?:(?!\\1).)*)\\1[^>]*>", Pattern.CASE_INSENSITIVE);
private static volatile boolean Running = false;
/** story hashes we need to fetch (from newly found stories) */ /** story hashes we need to fetch (from newly found stories) */
private static Set<String> Hashes; private static Set<String> Hashes;
static {Hashes = new HashSet<String>();} static {Hashes = new HashSet<String>();}
@ -33,11 +33,15 @@ public class OriginalTextService extends SubService {
@Override @Override
protected void exec() { protected void exec() {
while ((Hashes.size() > 0) || (PriorityHashes.size() > 0)) { activelyRunning = true;
if (parent.stopSync()) return; try {
gotWork(); while ((Hashes.size() > 0) || (PriorityHashes.size() > 0)) {
fetchBatch(PriorityHashes); if (parent.stopSync()) return;
fetchBatch(Hashes); fetchBatch(PriorityHashes);
fetchBatch(Hashes);
}
} finally {
activelyRunning = false;
} }
} }
@ -97,27 +101,10 @@ public class OriginalTextService extends SubService {
return (Hashes.size() + PriorityHashes.size()); return (Hashes.size() + PriorityHashes.size());
} }
@Override
public boolean haveWork() {
return (getPendingCount() > 0);
}
public static void clear() { public static void clear() {
Hashes.clear(); Hashes.clear();
PriorityHashes.clear(); PriorityHashes.clear();
} }
public static boolean running() {
return Running;
}
@Override
protected void setRunning(boolean running) {
Running = running;
}
@Override
public boolean isRunning() {
return Running;
}
} }

View file

@ -1,17 +0,0 @@
package com.newsblur.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class ServiceScheduleReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(this.getClass().getName(), "starting sync service");
Intent i = new Intent(context, NBSyncService.class);
context.startService(i);
}
}

View file

@ -1,14 +1,15 @@
package com.newsblur.service; package com.newsblur.service;
import android.os.Process; import android.os.Process;
import android.util.Log;
import com.newsblur.activity.NbActivity; import com.newsblur.activity.NbActivity;
import com.newsblur.util.AppConstants; import com.newsblur.util.AppConstants;
import com.newsblur.util.Log;
import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.RejectedExecutionException;
/** /**
* A utility construct to make NbSyncService a bit more modular by encapsulating sync tasks * A utility construct to make NbSyncService a bit more modular by encapsulating sync tasks
@ -19,8 +20,7 @@ import java.util.concurrent.TimeUnit;
public abstract class SubService { public abstract class SubService {
protected NBSyncService parent; protected NBSyncService parent;
private ExecutorService executor; private ThreadPoolExecutor executor;
protected int startId;
private long cycleStartTime = 0L; private long cycleStartTime = 0L;
private SubService() { private SubService() {
@ -29,15 +29,13 @@ public abstract class SubService {
SubService(NBSyncService parent) { SubService(NBSyncService parent) {
this.parent = parent; this.parent = parent;
executor = Executors.newFixedThreadPool(1); executor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
} }
public void start(final int startId) { public void start() {
if (parent.stopSync()) return;
parent.incrementRunningChild();
this.startId = startId;
Runnable r = new Runnable() { Runnable r = new Runnable() {
public void run() { public void run() {
if (parent.stopSync()) return;
if (NbActivity.getActiveActivityCount() < 1) { if (NbActivity.getActiveActivityCount() < 1) {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_LESS_FAVORABLE ); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_LESS_FAVORABLE );
} else { } else {
@ -45,44 +43,37 @@ public abstract class SubService {
} }
Thread.currentThread().setName(this.getClass().getName()); Thread.currentThread().setName(this.getClass().getName());
exec_(); exec_();
parent.decrementRunningChild(startId);
} }
}; };
executor.execute(r); try {
} executor.execute(r);
// enqueue a check task that will run strictly after the real one, so the callback
public void startConditional(int startId) { // can effectively check queue size to see if there are queued tasks
if (haveWork()) start(startId); executor.execute(new Runnable() {
} public void run() {
parent.checkCompletion();
/** NbActivity.updateAllActivities(NbActivity.UPDATE_STATUS);
* Stub - children should implement a queue check or ready check so that startConditional() }
* can more efficiently allocate threads. });
*/ } catch (RejectedExecutionException ree) {
protected boolean haveWork() { // this is perfectly normal, as service soft-stop mechanics might have shut down our thread pool
return true; // while peer subservices are still running
}
} }
private synchronized void exec_() { private synchronized void exec_() {
try { try {
//if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService started");
exec(); exec();
//if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService completed");
cycleStartTime = 0; cycleStartTime = 0;
} catch (Exception e) { } catch (Exception e) {
com.newsblur.util.Log.e(this.getClass().getName(), "Sync error.", e); com.newsblur.util.Log.e(this.getClass().getName(), "Sync error.", e);
} finally { }
if (isRunning()) {
setRunning(false);
NbActivity.updateAllActivities(NbActivity.UPDATE_STATUS);
}
}
} }
protected abstract void exec(); protected abstract void exec();
public void shutdown() { public void shutdown() {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService stopping"); Log.d(this, "SubService stopping");
executor.shutdown(); executor.shutdown();
try { try {
executor.awaitTermination(AppConstants.SHUTDOWN_SLACK_SECONDS, TimeUnit.SECONDS); executor.awaitTermination(AppConstants.SHUTDOWN_SLACK_SECONDS, TimeUnit.SECONDS);
@ -90,21 +81,18 @@ public abstract class SubService {
executor.shutdownNow(); executor.shutdownNow();
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} finally { } finally {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService stopped"); Log.d(this, "SubService stopped");
} }
} }
protected void gotWork() {
setRunning(true);
NbActivity.updateAllActivities(NbActivity.UPDATE_STATUS);
}
protected void gotData(int updateType) { protected void gotData(int updateType) {
NbActivity.updateAllActivities(updateType); NbActivity.updateAllActivities(updateType);
} }
protected abstract void setRunning(boolean running); public boolean isRunning() {
protected abstract boolean isRunning(); // don't advise completion until there are no tasks, or just one check task left
return (executor.getQueue().size() > 0);
}
/** /**
* If called at the beginning of an expensive loop in a SubService, enforces the maximum duty cycle * If called at the beginning of an expensive loop in a SubService, enforces the maximum duty cycle

View file

@ -16,7 +16,7 @@ import java.util.Set;
public class UnreadsService extends SubService { public class UnreadsService extends SubService {
private static volatile boolean Running = false; public static boolean activelyRunning = false;
private static volatile boolean doMetadata = false; private static volatile boolean doMetadata = false;
@ -30,17 +30,20 @@ public class UnreadsService extends SubService {
@Override @Override
protected void exec() { protected void exec() {
if (doMetadata) { activelyRunning = true;
gotWork(); try {
syncUnreadList(); if (doMetadata) {
doMetadata = false; syncUnreadList();
} doMetadata = false;
}
if (StoryHashQueue.size() > 0) { if (StoryHashQueue.size() > 0) {
getNewUnreadStories(); getNewUnreadStories();
parent.pushNotifications(); parent.pushNotifications();
}
} finally {
activelyRunning = false;
} }
} }
private void syncUnreadList() { private void syncUnreadList() {
@ -133,7 +136,6 @@ public class UnreadsService extends SubService {
boolean isEnableNotifications = PrefsUtils.isEnableNotifications(parent); boolean isEnableNotifications = PrefsUtils.isEnableNotifications(parent);
if (! (isOfflineEnabled || isEnableNotifications)) return; if (! (isOfflineEnabled || isEnableNotifications)) return;
gotWork();
startExpensiveCycle(); startExpensiveCycle();
List<String> hashBatch = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE); List<String> hashBatch = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE);
@ -161,8 +163,8 @@ public class UnreadsService extends SubService {
StoryHashQueue.remove(hash); StoryHashQueue.remove(hash);
} }
parent.prefetchOriginalText(response, startId); parent.prefetchOriginalText(response);
parent.prefetchImages(response, startId); parent.prefetchImages(response);
} }
} }
@ -202,17 +204,5 @@ public class UnreadsService extends SubService {
return doMetadata; return doMetadata;
} }
public static boolean running() {
return Running;
}
@Override
protected void setRunning(boolean running) {
Running = running;
}
@Override
protected boolean isRunning() {
return Running;
}
} }

View file

@ -49,7 +49,10 @@ public class FeedUtils {
} }
} }
private static void triggerSync(Context c) { public static void triggerSync(Context c) {
// NB: when our minSDKversion hits 28, it could be possible to start the service via the JobScheduler
// with the setImportantWhileForeground() flag via an enqueue() and get rid of all legacy startService
// code paths
Intent i = new Intent(c, NBSyncService.class); Intent i = new Intent(c, NBSyncService.class);
c.startService(i); c.startService(i);
} }
@ -330,6 +333,14 @@ public class FeedUtils {
triggerSync(context); triggerSync(context);
} }
public static void renameFeed(Context context, String feedId, String newFeedName) {
ReadingAction ra = ReadingAction.renameFeed(feedId, newFeedName);
dbHelper.enqueueAction(ra);
int impact = ra.doLocal(dbHelper);
NbActivity.updateAllActivities(impact);
triggerSync(context);
}
public static void unshareStory(Story story, Context context) { public static void unshareStory(Story story, Context context) {
ReadingAction ra = ReadingAction.unshareStory(story.storyHash, story.id, story.feedId); ReadingAction ra = ReadingAction.unshareStory(story.storyHash, story.id, story.feedId);
dbHelper.enqueueAction(ra); dbHelper.enqueueAction(ra);

View file

@ -50,7 +50,6 @@ public class PrefsUtils {
edit.putString(PrefConstants.PREF_COOKIE, cookie); edit.putString(PrefConstants.PREF_COOKIE, cookie);
edit.putString(PrefConstants.PREF_UNIQUE_LOGIN, userName + "_" + System.currentTimeMillis()); edit.putString(PrefConstants.PREF_UNIQUE_LOGIN, userName + "_" + System.currentTimeMillis());
edit.commit(); edit.commit();
NBSyncService.resumeFromInterrupt();
} }
public static boolean checkForUpgrade(Context context) { public static boolean checkForUpgrade(Context context) {
@ -126,8 +125,6 @@ public class PrefsUtils {
s.append("\n"); s.append("\n");
s.append("server: ").append(APIConstants.isCustomServer() ? "default" : "custom"); s.append("server: ").append(APIConstants.isCustomServer() ? "default" : "custom");
s.append("\n"); s.append("\n");
s.append("memory: ").append(NBSyncService.isMemoryLow() ? "low" : "normal");
s.append("\n");
s.append("speed: ").append(NBSyncService.getSpeedInfo()); s.append("speed: ").append(NBSyncService.getSpeedInfo());
s.append("\n"); s.append("\n");
s.append("pending actions: ").append(NBSyncService.getPendingInfo()); s.append("pending actions: ").append(NBSyncService.getPendingInfo());

View file

@ -40,11 +40,12 @@ public class ReadingAction implements Serializable {
UNMUTE_FEEDS, UNMUTE_FEEDS,
SET_NOTIFY, SET_NOTIFY,
INSTA_FETCH, INSTA_FETCH,
UPDATE_INTEL UPDATE_INTEL,
RENAME_FEED
}; };
private final long time; private long time;
private final int tried; private int tried;
private ActionType type; private ActionType type;
private String storyHash; private String storyHash;
private FeedSet feedSet; private FeedSet feedSet;
@ -59,6 +60,7 @@ public class ReadingAction implements Serializable {
private String notifyFilter; private String notifyFilter;
private List<String> notifyTypes; private List<String> notifyTypes;
private Classifier classifier; private Classifier classifier;
private String newFeedName;
// For mute/unmute the API call is always the active feed IDs. // For mute/unmute the API call is always the active feed IDs.
// We need the feed Ids being modified for the local call. // We need the feed Ids being modified for the local call.
@ -231,198 +233,34 @@ public class ReadingAction implements Serializable {
return ra; return ra;
} }
public static ReadingAction renameFeed(String feedId, String newFeedName) {
ReadingAction ra = new ReadingAction();
ra.type = ActionType.RENAME_FEED;
ra.feedId = feedId;
ra.newFeedName = newFeedName;
return ra;
}
public ContentValues toContentValues() { public ContentValues toContentValues() {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(DatabaseConstants.ACTION_TIME, time); values.put(DatabaseConstants.ACTION_TIME, time);
values.put(DatabaseConstants.ACTION_TRIED, tried); values.put(DatabaseConstants.ACTION_TRIED, tried);
values.put(DatabaseConstants.ACTION_TYPE, type.toString()); // because ReadingActions will have to represent a wide and ever-growing variety of interactions,
switch (type) { // the number of parameters will continue growing unbounded. to avoid having to frequently modify the
// database and support a table with dozens or hundreds of columns that are only ever used at a low
case MARK_READ: // cardinality, only the ACTION_TIME and ACTION_TRIED values are stored in columns of their own, and
if (storyHash != null) { // all remaining fields are frozen as JSON, since they are never queried upon.
values.put(DatabaseConstants.ACTION_STORY_HASH, storyHash); values.put(DatabaseConstants.ACTION_PARAMS, DatabaseConstants.JsonHelper.toJson(this));
} else if (feedSet != null) {
values.put(DatabaseConstants.ACTION_FEED_ID, feedSet.toCompactSerial());
if (olderThan != null) values.put(DatabaseConstants.ACTION_INCLUDE_OLDER, olderThan);
if (newerThan != null) values.put(DatabaseConstants.ACTION_INCLUDE_NEWER, newerThan);
}
break;
case MARK_UNREAD:
if (storyHash != null) {
values.put(DatabaseConstants.ACTION_STORY_HASH, storyHash);
}
break;
case SAVE:
values.put(DatabaseConstants.ACTION_STORY_HASH, storyHash);
break;
case UNSAVE:
values.put(DatabaseConstants.ACTION_STORY_HASH, storyHash);
break;
case SHARE:
values.put(DatabaseConstants.ACTION_STORY_HASH, storyHash);
values.put(DatabaseConstants.ACTION_STORY_ID, storyId);
values.put(DatabaseConstants.ACTION_FEED_ID, feedId);
values.put(DatabaseConstants.ACTION_SOURCE_USER_ID, sourceUserId);
values.put(DatabaseConstants.ACTION_COMMENT_TEXT, commentReplyText);
break;
case UNSHARE:
values.put(DatabaseConstants.ACTION_STORY_HASH, storyHash);
values.put(DatabaseConstants.ACTION_STORY_ID, storyId);
values.put(DatabaseConstants.ACTION_FEED_ID, feedId);
break;
case LIKE_COMMENT:
values.put(DatabaseConstants.ACTION_STORY_ID, storyId);
values.put(DatabaseConstants.ACTION_FEED_ID, feedId);
values.put(DatabaseConstants.ACTION_COMMENT_ID, commentUserId);
break;
case UNLIKE_COMMENT:
values.put(DatabaseConstants.ACTION_STORY_ID, storyId);
values.put(DatabaseConstants.ACTION_FEED_ID, feedId);
values.put(DatabaseConstants.ACTION_COMMENT_ID, commentUserId);
break;
case REPLY:
values.put(DatabaseConstants.ACTION_STORY_ID, storyId);
values.put(DatabaseConstants.ACTION_FEED_ID, feedId);
values.put(DatabaseConstants.ACTION_COMMENT_ID, commentUserId);
values.put(DatabaseConstants.ACTION_COMMENT_TEXT, commentReplyText);
break;
case EDIT_REPLY:
values.put(DatabaseConstants.ACTION_STORY_ID, storyId);
values.put(DatabaseConstants.ACTION_FEED_ID, feedId);
values.put(DatabaseConstants.ACTION_COMMENT_ID, commentUserId);
values.put(DatabaseConstants.ACTION_COMMENT_TEXT, commentReplyText);
values.put(DatabaseConstants.ACTION_REPLY_ID, replyId);
break;
case DELETE_REPLY:
values.put(DatabaseConstants.ACTION_STORY_ID, storyId);
values.put(DatabaseConstants.ACTION_FEED_ID, feedId);
values.put(DatabaseConstants.ACTION_COMMENT_ID, commentUserId);
values.put(DatabaseConstants.ACTION_REPLY_ID, replyId);
break;
case MUTE_FEEDS:
values.put(DatabaseConstants.ACTION_FEED_ID, DatabaseConstants.JsonHelper.toJson(activeFeedIds));
values.put(DatabaseConstants.ACTION_MODIFIED_FEED_IDS, DatabaseConstants.JsonHelper.toJson(modifiedFeedIds));
break;
case UNMUTE_FEEDS:
values.put(DatabaseConstants.ACTION_FEED_ID, DatabaseConstants.JsonHelper.toJson(activeFeedIds));
values.put(DatabaseConstants.ACTION_MODIFIED_FEED_IDS, DatabaseConstants.JsonHelper.toJson(modifiedFeedIds));
break;
case SET_NOTIFY:
values.put(DatabaseConstants.ACTION_FEED_ID, feedId);
values.put(DatabaseConstants.ACTION_NOTIFY_FILTER, notifyFilter);
values.put(DatabaseConstants.ACTION_NOTIFY_TYPES, DatabaseConstants.JsonHelper.toJson(notifyTypes));
break;
case INSTA_FETCH:
values.put(DatabaseConstants.ACTION_FEED_ID, feedId);
break;
case UPDATE_INTEL:
values.put(DatabaseConstants.ACTION_FEED_ID, feedId);
values.put(DatabaseConstants.ACTION_CLASSIFIER, DatabaseConstants.JsonHelper.toJson(classifier));
values.put(DatabaseConstants.ACTION_FEED_SET, feedSet.toCompactSerial());
break;
default:
throw new IllegalStateException("cannot serialise uknown type of action.");
}
return values; return values;
} }
public static ReadingAction fromCursor(Cursor c) { public static ReadingAction fromCursor(Cursor c) {
long time = c.getLong(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_TIME)); long time = c.getLong(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_TIME));
int tried = c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_TRIED)); int tried = c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_TRIED));
ReadingAction ra = new ReadingAction(time, tried); String params = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_PARAMS));
ra.type = ActionType.valueOf(c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_TYPE))); ReadingAction ra = DatabaseConstants.JsonHelper.fromJson(params, ReadingAction.class);
if (ra.type == ActionType.MARK_READ) { ra.time = time;
String hash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH)); ra.tried = tried;
String feedIds = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID));
Long includeOlder = DatabaseConstants.nullIfZero(c.getLong(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_INCLUDE_OLDER)));
Long includeNewer = DatabaseConstants.nullIfZero(c.getLong(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_INCLUDE_NEWER)));
if (hash != null) {
ra.storyHash = hash;
} else if (feedIds != null) {
ra.feedSet = FeedSet.fromCompactSerial(feedIds);
ra.olderThan = includeOlder;
ra.newerThan = includeNewer;
} else {
throw new IllegalStateException("cannot deserialise uknown type of action.");
}
} else if (ra.type == ActionType.MARK_UNREAD) {
ra.storyHash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
} else if (ra.type == ActionType.SAVE) {
ra.storyHash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
} else if (ra.type == ActionType.UNSAVE) {
ra.storyHash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
} else if (ra.type == ActionType.SHARE) {
ra.storyHash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
ra.storyId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_ID));
ra.feedId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID));
ra.sourceUserId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_SOURCE_USER_ID));
ra.commentReplyText = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_COMMENT_TEXT));
} else if (ra.type == ActionType.UNSHARE) {
ra.storyHash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
ra.storyId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_ID));
ra.feedId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID));
} else if (ra.type == ActionType.LIKE_COMMENT) {
ra.storyId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_ID));
ra.feedId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID));
ra.commentUserId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_COMMENT_ID));
} else if (ra.type == ActionType.UNLIKE_COMMENT) {
ra.storyId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_ID));
ra.feedId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID));
ra.commentUserId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_COMMENT_ID));
} else if (ra.type == ActionType.REPLY) {
ra.storyId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_ID));
ra.feedId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID));
ra.commentUserId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_COMMENT_ID));
ra.commentReplyText = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_COMMENT_TEXT));
} else if (ra.type == ActionType.EDIT_REPLY) {
ra.storyId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_ID));
ra.feedId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID));
ra.commentUserId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_COMMENT_ID));
ra.commentReplyText = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_COMMENT_TEXT));
ra.replyId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_REPLY_ID));
} else if (ra.type == ActionType.DELETE_REPLY) {
ra.storyId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_ID));
ra.feedId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID));
ra.commentUserId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_COMMENT_ID));
ra.replyId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_REPLY_ID));
} else if (ra.type == ActionType.MUTE_FEEDS) {
ra.activeFeedIds = DatabaseConstants.JsonHelper.fromJson(c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID)), Set.class);
ra.modifiedFeedIds = DatabaseConstants.JsonHelper.fromJson(c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_MODIFIED_FEED_IDS)), Set.class);
} else if (ra.type == ActionType.UNMUTE_FEEDS) {
ra.activeFeedIds = DatabaseConstants.JsonHelper.fromJson(c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID)), Set.class);
ra.modifiedFeedIds = DatabaseConstants.JsonHelper.fromJson(c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_MODIFIED_FEED_IDS)), Set.class);
} else if (ra.type == ActionType.SET_NOTIFY) {
ra.feedId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID));
ra.notifyFilter = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_NOTIFY_FILTER));
ra.notifyTypes = DatabaseConstants.JsonHelper.fromJson(c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_NOTIFY_TYPES)), List.class);
} else if (ra.type == ActionType.INSTA_FETCH) {
ra.feedId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID));
} else if (ra.type == ActionType.UPDATE_INTEL) {
ra.feedId = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID));
ra.classifier = DatabaseConstants.JsonHelper.fromJson(c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_CLASSIFIER)), Classifier.class);
String feedIds = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_SET));
ra.feedSet = FeedSet.fromCompactSerial(feedIds);
} else {
throw new IllegalStateException("cannot deserialise uknown type of action.");
}
return ra; return ra;
} }
@ -510,6 +348,10 @@ public class ReadingAction implements Serializable {
NBSyncService.addRecountCandidates(feedSet); NBSyncService.addRecountCandidates(feedSet);
break; break;
case RENAME_FEED:
result = apiManager.renameFeed(feedId, newFeedName);
break;
default: default:
throw new IllegalStateException("cannot execute uknown type of action."); throw new IllegalStateException("cannot execute uknown type of action.");
@ -644,6 +486,11 @@ public class ReadingAction implements Serializable {
impact |= NbActivity.UPDATE_INTEL; impact |= NbActivity.UPDATE_INTEL;
break; break;
case RENAME_FEED:
dbHelper.renameFeed(feedId, newFeedName);
impact |= NbActivity.UPDATE_METADATA;
break;
default: default:
// not all actions have these, which is fine // not all actions have these, which is fine
} }

View file

@ -3,6 +3,7 @@ package com.newsblur.view;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.View; import android.view.View;
@ -107,7 +108,10 @@ public class NewsblurWebview extends WebView {
@Override @Override
public void onReceivedError (WebView view, WebResourceRequest request, WebResourceError error) { public void onReceivedError (WebView view, WebResourceRequest request, WebResourceError error) {
com.newsblur.util.Log.w(this, "WebView Error ("+error.getErrorCode()+"): " + error.getDescription()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
com.newsblur.util.Log.w(this, "WebView Error ("+error.getErrorCode()+"): " + error.getDescription());
}
fragment.flagWebviewError();
} }
} }

View file

@ -247,7 +247,7 @@
if (!receipt) { if (!receipt) {
NSLog(@" No receipt found!"); NSLog(@" No receipt found!");
[self informError:@"No receipt found"]; [self informError:@"No receipt found"];
return; // return;
} }
NSString *urlString = [NSString stringWithFormat:@"%@/profile/save_ios_receipt/", NSString *urlString = [NSString stringWithFormat:@"%@/profile/save_ios_receipt/",
@ -283,6 +283,7 @@
}]; }];
} }
#pragma mark - Table Delegate #pragma mark - Table Delegate
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {

View file

@ -1900,19 +1900,6 @@ NEWSBLUR.AssetModel = Backbone.Router.extend({
}); });
}, },
fetch_original_story_page: function(story_hash, callback, error_callback) {
var story = this.get_story(story_hash);
this.make_request('/rss_feeds/original_story', {
story_hash: story_hash
}, function(data) {
story.set('original_page', data.original_page);
callback(data);
}, error_callback, {
request_type: 'GET',
ajax_group: 'statistics'
});
},
recalculate_story_scores: function(feed_id, options) { recalculate_story_scores: function(feed_id, options) {
options = options || {}; options = options || {};
this.stories.each(_.bind(function(story, i) { this.stories.each(_.bind(function(story, i) {

View file

@ -164,6 +164,9 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({
detail: {background: background} detail: {background: background}
}); });
var success = !this.story_title_view.$st.find('a')[0].dispatchEvent(event); var success = !this.story_title_view.$st.find('a')[0].dispatchEvent(event);
if (!success) {
success = this.story_title_view.$st.hasClass('NB-story-webkit-opened');
}
if (success) { if (success) {
// console.log(['Used safari extension to open link in background', success]); // console.log(['Used safari extension to open link in background', success]);
return; return;

View file

@ -88,7 +88,7 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, {
])), ])),
(!NEWSBLUR.Globals.is_premium && $.make('div', { className: 'NB-feedchooser-info'}, [ (!NEWSBLUR.Globals.is_premium && $.make('div', { className: 'NB-feedchooser-info'}, [
$.make('div', { className: 'NB-feedchooser-info-type' }, [ $.make('div', { className: 'NB-feedchooser-info-type' }, [
$.make('span', { className: 'NB-feedchooser-subtitle-type-prefix' }, 'Super-Mega'), $.make('span', { className: 'NB-feedchooser-subtitle-type-prefix' }, 'Unlimited'),
' Premium Account' ' Premium Account'
]) ])
])), ])),

View file

@ -582,7 +582,7 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
// Fudge factor is simply because it looks better at 64 pixels off. // Fudge factor is simply because it looks better at 64 pixels off.
// NEWSBLUR.log(['check_feed_view_scrolled_to_bottom', full_height, container_offset, visible_height, scroll_y, NEWSBLUR.reader.flags['opening_feed']]); // NEWSBLUR.log(['check_feed_view_scrolled_to_bottom', full_height, container_offset, visible_height, scroll_y, NEWSBLUR.reader.flags['opening_feed']]);
if ((visible_height + 64) >= full_height) { if ((visible_height + 2048) >= full_height) {
// NEWSBLUR.log(['check_feed_view_scrolled_to_bottom', full_height, container_offset, visible_height, scroll_y, NEWSBLUR.reader.flags['opening_feed']]); // NEWSBLUR.log(['check_feed_view_scrolled_to_bottom', full_height, container_offset, visible_height, scroll_y, NEWSBLUR.reader.flags['opening_feed']]);
NEWSBLUR.reader.load_page_of_feed_stories(); NEWSBLUR.reader.load_page_of_feed_stories();
} }

View file

@ -34,7 +34,7 @@
float: right;"> float: right;">
<div class="NB-feedchooser-info"> <div class="NB-feedchooser-info">
<div class="NB-feedchooser-info-type"> <div class="NB-feedchooser-info-type">
<span class="NB-feedchooser-subtitle-type-prefix">Super-Mega</span> Premium Account <span class="NB-feedchooser-subtitle-type-prefix">Unlimited</span> Premium Account
</div> </div>
</div> </div>
<ul class="NB-feedchooser-premium-bullets"> <ul class="NB-feedchooser-premium-bullets">

View file

@ -205,6 +205,10 @@ class FacebookFetcher:
def favicon_url(self): def favicon_url(self):
page_name = self.extract_page_name() page_name = self.extract_page_name()
facebook_user = self.facebook_user() facebook_user = self.facebook_user()
if not facebook_user:
logging.debug(u' ***> [%-30s] ~FRFacebook icon failed, disconnecting facebook: %s' %
(self.feed.log_title[:30], self.feed.feed_address))
return
try: try:
picture_data = facebook_user.get_object(page_name, fields='picture') picture_data = facebook_user.get_object(page_name, fields='picture')

View file

@ -505,6 +505,7 @@ class ProcessFeed:
stories = [] stories = []
for entry in self.fpf.entries: for entry in self.fpf.entries:
story = pre_process_story(entry, self.fpf.encoding) story = pre_process_story(entry, self.fpf.encoding)
if not story['title'] and not story['story_content']: continue
if story.get('published') < start_date: if story.get('published') < start_date:
start_date = story.get('published') start_date = story.get('published')
if replace_guids: if replace_guids:

View file

@ -2,6 +2,7 @@ import re
import datetime import datetime
import struct import struct
import dateutil import dateutil
from random import randint
from HTMLParser import HTMLParser from HTMLParser import HTMLParser
from lxml.html.diff import tokenize, fixup_ins_del_tags, htmldiff_tokens from lxml.html.diff import tokenize, fixup_ins_del_tags, htmldiff_tokens
from lxml.etree import ParserError, XMLSyntaxError from lxml.etree import ParserError, XMLSyntaxError
@ -87,7 +88,7 @@ def _extract_date_tuples(date):
return parsed_date, date_tuple, today_tuple, yesterday_tuple return parsed_date, date_tuple, today_tuple, yesterday_tuple
def pre_process_story(entry, encoding): def pre_process_story(entry, encoding):
publish_date = entry.get('published_parsed') or entry.get('updated_parsed') publish_date = entry.get('g_parsed') or entry.get('updated_parsed')
if publish_date: if publish_date:
publish_date = datetime.datetime(*publish_date[:6]) publish_date = datetime.datetime(*publish_date[:6])
if not publish_date and entry.get('published'): if not publish_date and entry.get('published'):
@ -99,7 +100,7 @@ def pre_process_story(entry, encoding):
if publish_date: if publish_date:
entry['published'] = publish_date entry['published'] = publish_date
else: else:
entry['published'] = datetime.datetime.utcnow() entry['published'] = datetime.datetime.utcnow() + datetime.timedelta(seconds=randint(0, 59))
if entry['published'] < datetime.datetime(2000, 1, 1): if entry['published'] < datetime.datetime(2000, 1, 1):
entry['published'] = datetime.datetime.utcnow() entry['published'] = datetime.datetime.utcnow()
@ -107,7 +108,7 @@ def pre_process_story(entry, encoding):
# Future dated stories get forced to current date # Future dated stories get forced to current date
# if entry['published'] > datetime.datetime.now() + datetime.timedelta(days=1): # if entry['published'] > datetime.datetime.now() + datetime.timedelta(days=1):
if entry['published'] > datetime.datetime.now(): if entry['published'] > datetime.datetime.now():
entry['published'] = datetime.datetime.now() entry['published'] = datetime.datetime.now() + datetime.timedelta(seconds=randint(0, 59))
# entry_link = entry.get('link') or '' # entry_link = entry.get('link') or ''
# protocol_index = entry_link.find("://") # protocol_index = entry_link.find("://")
@ -271,6 +272,10 @@ def linkify(*args, **kwargs):
return xhtml_unescape_tornado(linkify_tornado(*args, **kwargs)) return xhtml_unescape_tornado(linkify_tornado(*args, **kwargs))
def truncate_chars(value, max_length): def truncate_chars(value, max_length):
try:
value = value.encode('utf-8')
except UnicodeDecodeError:
pass
if len(value) <= max_length: if len(value) <= max_length:
return value return value
@ -280,7 +285,7 @@ def truncate_chars(value, max_length):
if rightmost_space != -1: if rightmost_space != -1:
truncd_val = truncd_val[:rightmost_space] truncd_val = truncd_val[:rightmost_space]
return truncd_val + "..." return truncd_val.decode('utf-8', 'ignore') + "..."
def image_size(datastream): def image_size(datastream):
datastream = reseekfile.ReseekFile(datastream) datastream = reseekfile.ReseekFile(datastream)