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.user_functions import get_user
from utils.user_functions import ajax_login_required
from utils.view_functions import render_to
def index(requst):
pass
@ -84,10 +83,13 @@ def save_classifier(request):
try:
classifier = ClassifierCls.objects.get(**classifier_dict)
except ClassifierCls.DoesNotExist:
classifier_dict.update(dict(score=score))
classifier = ClassifierCls.objects.create(**classifier_dict)
except NotUniqueError:
continue
classifier = None
if not classifier:
try:
classifier_dict.update(dict(score=score))
classifier = ClassifierCls.objects.create(**classifier_dict)
except NotUniqueError:
continue
if score == 0:
classifier.delete()
elif classifier.score != score:

View file

@ -1,5 +1,4 @@
import urllib
import urlparse
import datetime
import lxml.html
import tweepy
@ -284,7 +283,12 @@ def api_user_info(request):
@json.json_view
def api_feed_list(request, trigger_slug=None):
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()
titles = [dict(label=" - Folder: All Site Stories", value="all")]
feeds = {}

View file

@ -2,61 +2,62 @@ import stripe, datetime, time
from django.conf import settings
from django.core.management.base import BaseCommand
from django.contrib.auth.models import User
from optparse import make_option
from utils import log as logging
from apps.profile.models import Profile, PaymentHistory
from apps.profile.models import Profile
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
# 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("-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):
stripe.api_key = settings.STRIPE_SECRET
week = (datetime.datetime.now() - datetime.timedelta(days=int(options.get('days', 365)))).strftime('%s')
failed = []
limit = 100
offset = options.get('offset')
limit = options.get('limit')
starting_after = options.get('start')
i = 0
while True:
logging.debug(" ---> At %s" % offset)
logging.debug(" ---> At %s / %s" % (i, starting_after))
i += 1
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:
time.sleep(10)
continue
charges = data['data']
if not len(charges):
logging.debug("At %s, finished" % offset)
logging.debug("At %s (%s), finished" % (i, starting_after))
break
offset += limit
starting_after = charges[-1]["id"]
customers = [c['customer'] for c in charges if 'customer' in c]
for customer in customers:
if not customer:
print " ***> No customer!"
continue
try:
profile = Profile.objects.get(stripe_id=customer)
user = profile.user
except Profile.DoesNotExist:
logging.debug(" ***> Couldn't find stripe_id=%s" % customer)
failed.append(customer)
continue
except Profile.MultipleObjectsReturned:
logging.debug(" ***> Multiple stripe_id=%s" % customer)
failed.append(customer)
continue
try:
if not user.profile.is_premium:
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)
user.profile.setup_premium_history()
except stripe.APIConnectionError:
logging.debug(" ***> Failed: %s" % user.username)
failed.append(username)
failed.append(user.username)
time.sleep(2)
continue

View file

@ -348,20 +348,23 @@ def stripe_form(request):
user.profile.premium_expire > datetime.datetime.now())
# 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)
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:
error = "This card was declined."
else:
customer.default_card = card.id
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
else:
try:
customer = stripe.Customer.create(**{
'card': zebra_form.cleaned_data['stripe_token'],
'source': zebra_form.cleaned_data['stripe_token'],
'plan': zebra_form.cleaned_data['plan'],
'email': user.email,
'description': user.username,
@ -375,19 +378,28 @@ def stripe_form(request):
user.profile.activate_premium() # TODO: Remove, because webhooks are slow
success_updating = True
if success_updating and customer and customer.subscriptions.count == 0:
billing_cycle_anchor = "now"
if current_premium:
billing_cycle_anchor = user.profile.premium_expire.strftime('%s')
stripe.Subscription.create(
# Check subscription to ensure latest plan, otherwise cancel it and subscribe
if success_updating and customer and customer.subscriptions.total_count == 1:
subscription = customer.subscriptions.data[0]
if subscription['plan']['id'] != "newsblur-premium-36":
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,
billing_cycle_anchor=billing_cycle_anchor,
items=[
{
"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:
zebra_form = StripePlusPaymentForm(email=user.email, plan=plan)

View file

@ -1344,7 +1344,7 @@ def load_river_stories__redis(request):
starred_stories = MStarredStory.objects(
user_id=user.pk,
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,
user_tags=story.user_tags))
for story in starred_stories])

View file

@ -446,7 +446,7 @@ class Feed(models.Model):
return feed
@timelimit(5)
@timelimit(10)
def _feedfinder(url):
found_feed_urls = feedfinder.find_feeds(url)
return found_feed_urls
@ -1136,7 +1136,6 @@ class Feed(models.Model):
if getattr(settings, 'TEST_DEBUG', False):
print " ---> Testing feed fetch: %s" % self.log_title
options['force'] = False
# options['force_fp'] = True # No, why would this be needed?
original_feed_address = self.feed_address
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 * 24 # 514131 * 1440/(60*24) = 514131
if spd >= 1:
if subs > 10:
if subs >= 10:
total = 6
elif subs > 1:
total = 15
else:
total = 60
total = 45
elif spd > 0:
if subs > 1:
total = 60 - (spd * 60)
@ -2099,9 +2098,9 @@ class Feed(models.Model):
if len(fetch_history['push_history']):
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:
total = min(total, 60*12*1)
total = min(total, 60*6*1)
else:
total = min(total, 60*24*2)

View file

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

View file

@ -526,7 +526,8 @@ def original_story(request):
if not story:
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)

View file

@ -1,3 +1,5 @@
import os
import urlparse
import datetime
import time
import zlib
@ -2329,6 +2331,7 @@ class MSharedStory(mongo.DynamicDocument):
reverse=True)
self.image_sizes = image_sizes
self.image_count = len(image_sizes)
self.image_urls = image_sources
self.save()
logging.debug(" ---> ~SN~FGFetched image sizes on shared story: ~SB%s/%s images" %
@ -2788,14 +2791,38 @@ class MSocialServices(mongo.Document):
try:
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:
user = User.objects.get(pk=self.user_id)
logging.user(user, "~FRTwitter error: ~SB%s" % e)
return
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):
message = shared_story.generate_post_to_service_message(include_url=False)
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.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'])
logging.user(request, "~BB~FRSaving social profile")

View file

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

View file

@ -8,7 +8,7 @@ buildscript {
google()
}
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"
android:textSize="16sp" />
<android.support.v4.view.ViewPager
android:id="@+id/reading_pager"
<FrameLayout
android:id="@+id/activity_reading_container"
android:layout_width="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_height="match_parent"
style="?readingBackground"
>
>
<LinearLayout
android:layout_width="match_parent"
@ -18,9 +18,105 @@
android:orientation="vertical"
android:focusable="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
android:layout_width="match_parent"
@ -51,6 +147,20 @@
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
android:id="@+id/reading_webview"
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"
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"
android:title="@string/menu_notifications_choose" >
<menu>

View file

@ -64,6 +64,9 @@
<item android:id="@+id/menu_delete_feed"
android:title="@string/menu_delete_feed"
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"
android:title="@string/menu_instafetch_feed" />
<item android:id="@+id/menu_infrequent_cutoff"

View file

@ -7,63 +7,4 @@
android:showAsAction="always"
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>

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="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_title">All Stories</string>
@ -60,6 +61,7 @@
<string name="share_this_story">SHARE</string>
<string name="unshare">DELETE SHARE</string>
<string name="update_shared">UPDATE COMMENT</string>
<string name="feed_name_save">RENAME FEED</string>
<string name="save_this">SAVE</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_text">TEXT</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_unread">UNREAD</string>
@ -156,13 +158,13 @@
<string name="menu_instafetch_feed">Insta-fetch stories</string>
<string name="menu_infrequent_cutoff">Infrequent stories per month</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="list_style_list">List</string>
<string name="list_style_grid_f">Grid (fine)</string>
<string name="list_style_grid_m">Grid (medium)</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="logout_warning">Are you sure you want to log out?</string>

View file

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

View file

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

View file

@ -3,13 +3,13 @@ package com.newsblur.activity;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import com.newsblur.R;
import com.newsblur.domain.Feed;
import com.newsblur.fragment.DeleteFeedFragment;
import com.newsblur.fragment.FeedIntelTrainerFragment;
import com.newsblur.fragment.RenameFeedFragment;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.UIUtils;
@ -66,6 +66,13 @@ public class FeedItemsList extends ItemsList {
intelFrag.show(getSupportFragmentManager(), FeedIntelTrainerFragment.class.getName());
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;
}

View file

@ -6,7 +6,6 @@ import android.view.MenuItem;
import com.newsblur.R;
import com.newsblur.fragment.InfrequentCutoffDialogFragment;
import com.newsblur.fragment.InfrequentCutoffDialogFragment.InfrequentCutoffChangedListener;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
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_instafetch_feed).setVisible(false);
menu.findItem(R.id.menu_intel).setVisible(false);
menu.findItem(R.id.menu_rename_feed).setVisible(false);
}
if (!fs.isInfrequent()) {

View file

@ -1,12 +1,9 @@
package com.newsblur.activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.widget.Toast;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
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.
*/
protected void triggerSync() {
Intent i = new Intent(this, NBSyncService.class);
startService(i);
FeedUtils.triggerSync(this);
}
/**

View file

@ -5,11 +5,10 @@ import java.util.List;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.view.ViewPager;
@ -34,10 +33,7 @@ import com.newsblur.R;
import com.newsblur.database.ReadingAdapter;
import com.newsblur.domain.Story;
import com.newsblur.fragment.ReadingItemFragment;
import com.newsblur.fragment.ShareDialogFragment;
import com.newsblur.fragment.StoryIntelTrainerFragment;
import com.newsblur.fragment.ReadingFontDialogFragment;
import com.newsblur.fragment.TextSizeDialogFragment;
import com.newsblur.fragment.ReadingPagerFragment;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.AppConstants;
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;
ViewPager pager;
ReadingPagerFragment readingFragment;
protected FragmentManager fragmentManager;
protected ReadingAdapter readingAdapter;
private boolean stopLoading;
protected FeedSet fs;
@ -126,8 +122,6 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
setContentView(R.layout.activity_reading);
ButterKnife.bind(this);
fragmentManager = getSupportFragmentManager();
try {
fs = (FeedSet)getIntent().getSerializableExtra(EXTRA_FEEDSET);
} catch (RuntimeException re) {
@ -185,24 +179,21 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
enableProgressCircle(overlayProgressLeft, false);
enableProgressCircle(overlayProgressRight, false);
boolean showFeedMetadata = true;
if (fs.isSingleNormal()) showFeedMetadata = false;
String sourceUserId = null;
if (fs.getSingleSocialFeed() != null) sourceUserId = fs.getSingleSocialFeed().getKey();
readingAdapter = new ReadingAdapter(getSupportFragmentManager(), sourceUserId, showFeedMetadata);
setupPager();
FragmentManager fragmentManager = getSupportFragmentManager();
ReadingPagerFragment fragment = (ReadingPagerFragment) fragmentManager.findFragmentByTag(ReadingPagerFragment.class.getName());
if (fragment == null) {
fragment = ReadingPagerFragment.newInstance();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.activity_reading_container, fragment, ReadingPagerFragment.class.getName());
transaction.commit();
}
getSupportLoaderManager().initLoader(0, null, this);
}
@Override
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);
*/
if (storyHash != null) {
outState.putString(EXTRA_STORY_HASH, storyHash);
} else {
@ -273,41 +264,37 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
return;
}
//NB: this implicitly calls readingAdapter.notifyDataSetChanged();
readingAdapter.swapCursor(cursor);
boolean lastCursorWasStale = (stories == null);
if (readingAdapter != null) {
// swapCursor() will asynch process the new cursor and fully update the pager,
// update child fragments, and then call pagerUpdated()
readingAdapter.swapCursor(cursor, pager);
}
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());
if (cursor.getCount() < 1) {
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() {
// if we already started and found our target story, this will be unset
if (storyHash == null) return;
@ -321,8 +308,6 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
if (stopLoading) return;
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);
this.onPageSelected(position);
// 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);
}
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
pager.setVisibility(View.INVISIBLE);
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.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);
// 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
public boolean onPrepareOptionsMenu(Menu 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;
}
@Override
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) {
finish();
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) {
ViewUtils.hideSystemUI(getWindow().getDecorView());
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 {
return super.onOptionsItemSelected(item);
}
@ -534,7 +458,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
if (readingAdapter == null) return null;
Story story = readingAdapter.getStory(position);
if (story != null) {
markStoryRead(story);
FeedUtils.markStoryAsRead(story, Reading.this);
synchronized (pageHistory) {
// 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)))) {
@ -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
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {

View file

@ -1532,6 +1532,12 @@ public class BlurDatabaseHelper {
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) {
if (c == null) return;
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_TIME = "time";
public static final String ACTION_TRIED = "tried";
public static final String ACTION_TYPE = "action_type";
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 ACTION_PARAMS = "action_params";
public static final String STARREDCOUNTS_TABLE = "starred_counts";
public static final String STARREDCOUNTS_COUNT = "count";
@ -302,21 +288,7 @@ public class DatabaseConstants {
ACTION_ID + INTEGER + " PRIMARY KEY AUTOINCREMENT, " +
ACTION_TIME + INTEGER + " NOT NULL, " +
ACTION_TRIED + INTEGER + ", " +
ACTION_TYPE + 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 +
ACTION_PARAMS + TEXT +
")";
static final String STARREDCOUNTS_SQL = "CREATE TABLE " + STARREDCOUNTS_TABLE + " (" +

View file

@ -1,114 +1,258 @@
package com.newsblur.database;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.util.SparseArray;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.PagerAdapter;
import android.view.View;
import android.view.ViewGroup;
import com.newsblur.activity.NbActivity;
import com.newsblur.activity.Reading;
import com.newsblur.domain.Classifier;
import com.newsblur.domain.Story;
import com.newsblur.fragment.LoadingFragment;
import com.newsblur.fragment.ReadingItemFragment;
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 boolean showFeedMetadata;
private SparseArray<WeakReference<ReadingItemFragment>> cachedFragments;
public ReadingAdapter(FragmentManager fm, String sourceUserId, boolean showFeedMetadata) {
super(fm);
this.cachedFragments = new SparseArray<WeakReference<ReadingItemFragment>>();
private Reading activity;
private FragmentManager fm;
private FragmentTransaction curTransaction = null;
private Fragment lastActiveFragment = null;
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.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
public synchronized Fragment getItem(int position) {
if (stories == null || stories.getCount() == 0 || position >= stories.getCount()) {
return new LoadingFragment();
} else {
stories.moveToPosition(position);
Story story = Story.fromCursor(stories);
story.bindExternValues(stories);
// TODO: does the pager generate new fragments in the UI thread? If so, classifiers should
// be loaded async by the fragment itself
Classifier classifier = FeedUtils.dbHelper.getClassifierForFeed(story.feedId);
return ReadingItemFragment.newInstance(story,
story.extern_feedTitle,
story.extern_feedColor,
story.extern_feedFade,
story.extern_faviconBorderColor,
story.extern_faviconTextColor,
story.extern_faviconUrl,
classifier,
showFeedMetadata,
sourceUserId);
}
private ReadingItemFragment createFragment(Story story) {
return ReadingItemFragment.newInstance(story,
story.extern_feedTitle,
story.extern_feedColor,
story.extern_feedFade,
story.extern_faviconBorderColor,
story.extern_faviconTextColor,
story.extern_faviconUrl,
classifiers.get(story.feedId),
showFeedMetadata,
sourceUserId);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
Object o = super.instantiateItem(container, position);
if (o instanceof ReadingItemFragment) {
cachedFragments.put(position, new WeakReference((ReadingItemFragment) o));
Story story = getStory(position);
Fragment fragment = null;
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
public void destroyItem(ViewGroup container, int position, Object object) {
cachedFragments.remove(position);
try {
super.destroyItem(container, position, object);
} catch (IllegalStateException ise) {
// it appears that sometimes the pager impatiently deletes stale fragments befre
// even calling it's own destroyItem method. we're just passing up the stack
// after evicting our cache, so don't expose this internal bug from our call stack
com.newsblur.util.Log.w(this, "ViewPager adapter rejected own destruction call.");
Fragment fragment = (Fragment) object;
if (curTransaction == null) {
curTransaction = fm.beginTransaction();
}
curTransaction.remove(fragment);
if (fragment instanceof ReadingItemFragment) {
ReadingItemFragment rif = (ReadingItemFragment) fragment;
if (rif.isAdded()) {
states.put(rif.story.storyHash, fm.saveFragmentInstanceState(rif));
}
fragments.remove(rif.story.storyHash);
}
}
public synchronized void swapCursor(Cursor cursor) {
this.stories = cursor;
notifyDataSetChanged();
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
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) {
if (stories == null || stories.isClosed() || stories.getColumnCount() == 0 || position >= stories.getCount() || position < 0) {
return null;
} else {
stories.moveToPosition(position);
Story story = Story.fromCursor(stories);
return story;
}
}
@Override
public void finishUpdate(ViewGroup container) {
if (curTransaction != null) {
curTransaction.commitNowAllowingStateLoss();
curTransaction = null;
}
}
public synchronized int getPosition(Story story) {
if (stories == null) return -1;
if (stories.isClosed()) return -1;
@Override
public boolean isViewFromObject(View view, Object object) {
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;
while (pos < stories.getCount()) {
stories.moveToPosition(pos);
if (Story.fromCursor(stories).equals(story)) {
while (pos < stories.size()) {
if (stories.get(pos).equals(story)) {
return pos;
}
pos++;
@ -117,56 +261,96 @@ public class ReadingAdapter extends FragmentStatePagerAdapter {
}
@Override
public synchronized int getItemPosition(Object object) {
if (object instanceof LoadingFragment) {
return POSITION_NONE;
} else {
return POSITION_UNCHANGED;
public int getItemPosition(Object object) {
if (object instanceof ReadingItemFragment) {
ReadingItemFragment rif = (ReadingItemFragment) object;
int pos = findHash(rif.story.storyHash);
if (pos >=0) return pos;
}
return POSITION_NONE;
}
public String getSourceUserId() {
return sourceUserId;
}
public synchronized ReadingItemFragment getExistingItem(int pos) {
WeakReference<ReadingItemFragment> frag = cachedFragments.get(pos);
if (frag == null) return null;
return frag.get();
public ReadingItemFragment getExistingItem(int pos) {
Story story = getStory(pos);
if (story == null) return null;
return fragments.get(story.storyHash);
}
@Override
public synchronized void notifyDataSetChanged() {
public void notifyDataSetChanged() {
super.notifyDataSetChanged();
// go one step further than the default pageradapter and also refresh the
// story object inside each fragment we have active
if (stories == null) return;
for (int i=0; i<stories.getCount(); i++) {
WeakReference<ReadingItemFragment> frag = cachedFragments.get(i);
if (frag == null) continue;
ReadingItemFragment rif = frag.get();
if (rif == null) continue;
rif.offerStoryUpdate(getStory(i));
rif.handleUpdate(NbActivity.UPDATE_STORY);
for (Story s : stories) {
ReadingItemFragment rif = fragments.get(s.storyHash);
if (rif != null ) {
rif.offerStoryUpdate(s);
rif.handleUpdate(NbActivity.UPDATE_STORY);
}
}
}
public synchronized int findFirstUnread() {
stories.moveToPosition(-1);
while (stories.moveToNext()) {
Story story = Story.fromCursor(stories);
if (!story.read) return stories.getPosition();
public int findFirstUnread() {
int pos = 0;
while (pos < stories.size()) {
if (! stories.get(pos).read) {
return pos;
}
pos++;
}
return -1;
}
public synchronized int findHash(String storyHash) {
stories.moveToPosition(-1);
while (stories.moveToNext()) {
Story story = Story.fromCursor(stories);
if (story.storyHash.equals(storyHash)) return stories.getPosition();
public int findHash(String storyHash) {
int pos = 0;
while (pos < stories.size()) {
if (stories.get(pos).storyHash.equals(storyHash)) {
return pos;
}
pos++;
}
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;
inflater.inflate(R.menu.context_feed, menu);
if (adapter.isRowAllSharedStories(groupPosition)) {
// social feeds
menu.removeItem(R.id.menu_delete_feed);
menu.removeItem(R.id.menu_choose_folders);
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_instafetch_feed);
menu.removeItem(R.id.menu_intel);
menu.removeItem(R.id.menu_rename_feed);
} else {
// normal feeds
menu.removeItem(R.id.menu_unfollow);
Feed feed = adapter.getFeed(groupPosition, childPosition);
@ -352,6 +355,12 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
DialogFragment chooseFoldersFragment = ChooseFoldersFragment.newInstance(feed);
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) {
Set<String> feedIds = new HashSet<String>();
feedIds.add(adapter.getFeed(groupPosition, childPosition).feedId);

View file

@ -159,11 +159,7 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
calcColumnCount(listStyle);
layoutManager = new GridLayoutManager(getActivity(), columnCount);
itemGrid.setLayoutManager(layoutManager);
RecyclerView.ItemAnimator anim = itemGrid.getItemAnimator();
// 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));
setupAnimSpeeds();
calcGridSpacing(listStyle);
itemGrid.addItemDecoration(new RecyclerView.ItemDecoration() {
@ -227,6 +223,8 @@ public class ItemSetFragment extends NbFragment implements LoaderManager.LoaderC
public void storyThawCompleted(int indexOfLastUnread) {
this.indexOfLastUnread = indexOfLastUnread;
this.fullFlingComplete = false;
// we don't actually calculate list speed until it has some stories
setupAnimSpeeds();
}
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) {
// the framework likes to trigger this on init before we even known counts, so disregard those
if (!cursorSeenYet) return;

View file

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

View file

@ -1,12 +1,9 @@
package com.newsblur.fragment;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedUtils;
public class NbFragment extends Fragment {
@ -16,21 +13,8 @@ public class NbFragment extends Fragment {
protected void triggerSync() {
Activity a = getActivity();
if (a != null) {
Intent i = new Intent(a, NBSyncService.class);
a.startService(i);
FeedUtils.triggerSync(a);
}
}
@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.GestureDetector;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
@ -26,8 +28,9 @@ import android.view.ViewGroup;
import android.webkit.WebView.HitTestResult;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import butterknife.ButterKnife;
import butterknife.Bind;
@ -40,7 +43,6 @@ import com.newsblur.domain.Classifier;
import com.newsblur.domain.Story;
import com.newsblur.domain.UserDetails;
import com.newsblur.service.OriginalTextService;
import com.newsblur.util.AppConstants;
import com.newsblur.util.DefaultFeedView;
import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils;
@ -59,8 +61,9 @@ import java.util.HashMap;
import java.util.regex.Matcher;
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_VALUE = "textSizeChangeValue";
public static final String READING_FONT_CHANGED = "readingFontChanged";
@ -71,7 +74,7 @@ public class ReadingItemFragment extends NbFragment {
private Classifier classifier;
@Bind(R.id.reading_webview) NewsblurWebview web;
@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;
@Bind(R.id.reading_item_title) TextView itemTitle;
@Bind(R.id.reading_item_authors) TextView itemAuthors;
@ -83,8 +86,10 @@ public class ReadingItemFragment extends NbFragment {
private DefaultFeedView selectedFeedView;
private boolean textViewUnavailable;
@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.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. */
private String storyContent;
@ -102,6 +107,7 @@ public class ReadingItemFragment extends NbFragment {
private boolean isWebLoadFinished;
private boolean isSocialLoadFinished;
private Boolean isLoadFinished = false;
private float savedScrollPosRel = 0f;
private final Object WEBVIEW_CONTENT_MUTEX = new Object();
@ -129,8 +135,6 @@ public class ReadingItemFragment extends NbFragment {
super.onCreate(savedInstanceState);
story = getArguments() != null ? (Story) getArguments().getSerializable("story") : null;
inflater = getActivity().getLayoutInflater();
displayFeedDetails = getArguments().getBoolean("displayFeedDetails");
user = PrefsUtils.getUserDetails(getActivity());
@ -150,11 +154,19 @@ public class ReadingItemFragment extends NbFragment {
getActivity().registerReceiver(textSizeReceiver, new IntentFilter(TEXT_SIZE_CHANGED));
readingFontReceiver = new ReadingFontReceiver();
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
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
@ -182,7 +194,8 @@ public class ReadingItemFragment extends NbFragment {
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);
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() {
if (story.starred) {
FeedUtils.setStorySaved(story, false, getActivity());
@ -500,25 +597,38 @@ public class ReadingItemFragment extends NbFragment {
}
private void reloadStoryContent() {
if ((selectedFeedView == DefaultFeedView.STORY) || textViewUnavailable) {
textViewLoadingMsg.setVisibility(View.GONE);
enableProgress(false);
// reset indicators
textViewLoadingMsg.setVisibility(View.GONE);
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) {
loadStoryContent();
} else {
setupWebview(storyContent);
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;
}
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) {
@ -575,7 +685,6 @@ public class ReadingItemFragment extends NbFragment {
if (OriginalTextService.NULL_STORY_TEXT.equals(result)) {
// 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);
UIUtils.safeToast(getActivity(), R.string.text_mode_unavailable, Toast.LENGTH_SHORT);
textViewUnavailable = true;
} else {
ReadingItemFragment.this.originalText = result;
@ -583,7 +692,6 @@ public class ReadingItemFragment extends NbFragment {
reloadStoryContent();
} else {
com.newsblur.util.Log.d(this, "orig text not yet cached for story: " + story.storyHash);
textViewLoadingMsg.setVisibility(View.VISIBLE);
OriginalTextService.addPriorityHash(story.storyHash);
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() {
// 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 {

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_SET_NOTIFICATIONS = "/notifications/feed/";
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) {
return CurrentUrlBase + path;
@ -118,6 +119,7 @@ public class APIConstants {
public static final String PARAMETER_NOTIFICATION_FILTER = "notification_filter";
public static final String PARAMETER_RESET_FETCH = "reset_fetch";
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_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);
}
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 */
private APIResponse get(final String urlString) {

View file

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

View file

@ -6,7 +6,7 @@ import com.newsblur.util.PrefsUtils;
public class CleanupService extends SubService {
private static volatile boolean Running = false;
public static boolean activelyRunning = false;
public CleanupService(NBSyncService parent) {
super(parent);
@ -14,16 +14,17 @@ public class CleanupService extends SubService {
@Override
protected void exec() {
gotWork();
if (PrefsUtils.isTimeToCleanup(parent)) {
com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up old stories");
parent.dbHelper.cleanupVeryOldStories();
if (!PrefsUtils.isKeepOldStories(parent)) {
parent.dbHelper.cleanupReadStories();
}
PrefsUtils.updateLastCleanupTime(parent);
if (!PrefsUtils.isTimeToCleanup(parent)) return;
activelyRunning = true;
com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up old stories");
parent.dbHelper.cleanupVeryOldStories();
if (!PrefsUtils.isKeepOldStories(parent)) {
parent.dbHelper.cleanupReadStories();
}
PrefsUtils.updateLastCleanupTime(parent);
com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up old story texts");
parent.dbHelper.cleanupStoryText();
@ -42,19 +43,9 @@ public class CleanupService extends SubService {
com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up thumbnail cache");
FileCache thumbCache = FileCache.asThumbnailCache(parent);
thumbCache.cleanupUnusedAndOld(parent.dbHelper.getAllStoryThumbnails(), PrefsUtils.getMaxCachedAgeMillis(parent));
}
public static boolean running() {
return Running;
activelyRunning = false;
}
@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 {
private static volatile boolean Running = false;
public static boolean activelyRunning = false;
FileCache storyImageCache;
FileCache thumbnailCache;
@ -33,77 +33,77 @@ public class ImagePrefetchService extends SubService {
@Override
protected void exec() {
if (!PrefsUtils.isImagePrefetchEnabled(parent)) return;
if (!PrefsUtils.isBackgroundNetworkAllowed(parent)) return;
activelyRunning = true;
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) {
if (! PrefsUtils.isImagePrefetchEnabled(parent)) return;
if (! PrefsUtils.isBackgroundNetworkAllowed(parent)) return;
startExpensiveCycle();
com.newsblur.util.Log.d(this, "story images 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.getAllStoryImages();
Set<String> fetchedImages = new HashSet<String>();
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);
startExpensiveCycle();
com.newsblur.util.Log.d(this, "story images 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.getAllStoryImages();
Set<String> fetchedImages = new HashSet<String>();
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;
}
} finally {
StoryImageQueue.removeAll(fetchedImages);
com.newsblur.util.Log.d(this, "story images 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);
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);
}
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) {
@ -118,27 +118,10 @@ public class ImagePrefetchService extends SubService {
return (StoryImageQueue.size() + ThumbnailQueue.size());
}
@Override
public boolean haveWork() {
return (getPendingCount() > 0);
}
public static void clear() {
StoryImageQueue.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;
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.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.Process;
import android.util.Log;
import com.newsblur.R;
import com.newsblur.activity.NbActivity;
@ -31,6 +29,7 @@ import com.newsblur.util.AppConstants;
import com.newsblur.util.DefaultFeedView;
import com.newsblur.util.FeedSet;
import com.newsblur.util.FileCache;
import com.newsblur.util.Log;
import com.newsblur.util.NetworkUtils;
import com.newsblur.util.NotificationUtils;
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
* 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 volatile static boolean ActionsRunning = false;
@ -88,7 +87,6 @@ public class NBSyncService extends Service {
public volatile static Boolean isPremium = null;
public volatile static Boolean isStaff = null;
private volatile static boolean isMemoryLow = false;
private static long lastFeedCount = 0L;
private static long lastFFConnMillis = 0L;
private static long lastFFReadMillis = 0L;
@ -130,16 +128,18 @@ public class NBSyncService extends Service {
Set<String> disabledFeedIds = new HashSet<String>();
private ExecutorService primaryExecutor;
private List<Integer> outstandingStartIds = new ArrayList<Integer>();
private List<JobParameters> outstandingStartParams = new ArrayList<JobParameters>();
private boolean mainSyncRunning = false;
CleanupService cleanupService;
OriginalTextService originalTextService;
UnreadsService unreadsService;
ImagePrefetchService imagePrefetchService;
private boolean forceHalted = false;
PowerManager.WakeLock wl = null;
APIManager apiManager;
BlurDatabaseHelper dbHelper;
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
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();
com.newsblur.util.Log.d(this, "onCreate");
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);
}
@ -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.
*/
private void finishConstruction() {
if (apiManager == null) {
if ((apiManager == null) || (dbHelper == null)) {
apiManager = new APIManager(this);
dbHelper = new BlurDatabaseHelper(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
* that the service should check for outstanding work.
* Kickoff hook for when we are started via Context.startService()
*/
@Override
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
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() {
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);
} else {
com.newsblur.util.Log.d(this, "Skipping sync: app not active and background sync not enabled.");
stopSelf(startId);
com.newsblur.util.Log.i(this, "Skipping sync: app not active and background sync not enabled.");
synchronized (COMPLETION_CALLBACKS_MUTEX) {outstandingStartIds.add(startId);}
checkCompletion();
}
// indicate to the system that the service should be alive when started, but
// needn't necessarily persist under memory pressure
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.
*/
private synchronized void doSync(final int startId) {
private synchronized void doSync() {
try {
if (HaltNow) return;
incrementRunningChild();
finishConstruction();
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "starting primary sync");
Log.d(this, "starting primary sync");
if (NbActivity.getActiveActivityCount() < 1) {
// 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);
// 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
syncActions();
// 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
syncPendingFeedStories(startId);
syncPendingFeedStories();
// re-apply the local state of any actions executed before local UI interaction
finishActions();
@ -268,20 +311,18 @@ public class NBSyncService extends Service {
checkRecounts();
// async story and image prefetch are lower priority and don't affect active reading, do them last
unreadsService.start(startId);
imagePrefetchService.start(startId);
unreadsService.start();
imagePrefetchService.start();
// 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
pushNotifications();
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "finishing primary sync");
Log.d(this, "finishing primary sync");
} catch (Exception 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 (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;
for (ReadingAction ra : FollowupActions) {
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
* unread hashes. Doing this resets pagination on the server!
*/
private void syncMetadata(int startId) {
private void syncMetadata() {
if (stopSync()) return;
if (backoffBackgroundCalls()) return;
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());
UnreadsService.doMetadata();
unreadsService.start(startId);
cleanupService.start(startId);
unreadsService.start();
cleanupService.start();
} finally {
FFSyncRunning = false;
@ -654,7 +695,7 @@ public class NBSyncService extends Service {
/**
* 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
// more to do, in which case we will clear it
boolean finished = false;
@ -679,6 +720,7 @@ public class NBSyncService extends Service {
}
if (fs == null) {
com.newsblur.util.Log.d(this.getClass().getName(), "No feed set to sync");
return;
}
@ -731,7 +773,7 @@ public class NBSyncService extends Service {
finishActions();
NbActivity.updateAllActivities(NbActivity.UPDATE_STORY | NbActivity.UPDATE_STATUS);
prefetchOriginalText(apiResponse, startId);
prefetchOriginalText(apiResponse);
FeedPagesSeen.put(fs, pageNumber);
totalStoriesSeen += apiResponse.stories.length;
@ -843,7 +885,7 @@ public class NBSyncService extends Service {
dbHelper.insertStories(apiResponse, false);
}
void prefetchOriginalText(StoriesResponse apiResponse, int startId) {
void prefetchOriginalText(StoriesResponse apiResponse) {
storyloop: for (Story story : apiResponse.stories) {
// only prefetch for unreads, so we don't grind to cache when the user scrolls
// 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) {
// only prefetch for unreads, so we don't grind to cache when the user scrolls
// through old read stories
@ -874,7 +916,7 @@ public class NBSyncService extends Service {
imagePrefetchService.addThumbnailUrl(story.thumbnailUrl);
}
}
imagePrefetchService.startConditional(startId);
imagePrefetchService.start();
}
void pushNotifications() {
@ -892,27 +934,28 @@ public class NBSyncService extends Service {
closeQuietly(cUnread);
}
void incrementRunningChild() {
synchronized (WAKELOCK_MUTEX) {
wl.acquire();
}
}
void decrementRunningChild(int startId) {
synchronized (WAKELOCK_MUTEX) {
if (wl == null) return;
if (wl.isHeld()) {
wl.release();
/**
* Check to see if all async sync tasks have completed, indicating that sync can me marked as
* complete. Call this any time any individual sync task finishes.
*/
void checkCompletion() {
//Log.d(this, "checking completion");
if (mainSyncRunning) return;
if ((cleanupService != null) && cleanupService.isRunning()) return;
if ((originalTextService != null) && originalTextService.isRunning()) return;
if ((unreadsService != null) && unreadsService.isRunning()) return;
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
// we are the last thread to release the lock.
if (!wl.isHeld()) {
if (NbActivity.getActiveActivityCount() < 1) {
stopSelf(startId);
}
lastStartIdCompleted = startId;
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "wakelock depleted");
for (Integer startId : outstandingStartIds) {
stopSelf(startId);
}
outstandingStartIds.clear();
outstandingStartParams.clear();
}
}
@ -945,18 +988,6 @@ public class NBSyncService extends Service {
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?
*/
@ -994,14 +1025,14 @@ public class NBSyncService extends Service {
if (OfflineNow) return context.getResources().getString(R.string.sync_status_offline);
if (HousekeepingRunning) return context.getResources().getString(R.string.sync_status_housekeeping);
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 (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 (StorySyncRunning) return context.getResources().getString(R.string.sync_status_stories);
if (UnreadsService.running()) return String.format(context.getResources().getString(R.string.sync_status_unreads), UnreadsService.getPendingCount());
if (OriginalTextService.running()) return String.format(context.getResources().getString(R.string.sync_status_text), OriginalTextService.getPendingCount());
if (ImagePrefetchService.running()) return String.format(context.getResources().getString(R.string.sync_status_images), ImagePrefetchService.getPendingCount());
if (UnreadsService.activelyRunning) return String.format(context.getResources().getString(R.string.sync_status_unreads), UnreadsService.getPendingCount());
if (OriginalTextService.activelyRunning) return String.format(context.getResources().getString(R.string.sync_status_text), OriginalTextService.getPendingCount());
if (ImagePrefetchService.activelyRunning) return String.format(context.getResources().getString(R.string.sync_status_images), ImagePrefetchService.getPendingCount());
return null;
}
@ -1047,7 +1078,7 @@ public class NBSyncService extends Service {
PendingFeed = fs;
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)) {
return true;
@ -1144,15 +1175,15 @@ public class NBSyncService extends Service {
ImagePrefetchService.clear();
}
public static void resumeFromInterrupt() {
HaltNow = false;
}
@Override
public void onDestroy() {
try {
com.newsblur.util.Log.d(this, "onDestroy - stopping execution");
HaltNow = true;
com.newsblur.util.Log.d(this, "onDestroy");
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 (unreadsService != null) unreadsService.shutdown();
if (originalTextService != null) originalTextService.shutdown();
@ -1166,21 +1197,15 @@ public class NBSyncService extends Service {
Thread.currentThread().interrupt();
}
}
if (dbHelper != null) dbHelper.close();
com.newsblur.util.Log.d(this, "onDestroy - execution halted");
super.onDestroy();
if (dbHelper != null) {
dbHelper.close();
dbHelper = null;
}
com.newsblur.util.Log.d(this, "onDestroy done");
} catch (Exception ex) {
com.newsblur.util.Log.e(this, "unclean shutdown", ex);
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
public static boolean isMemoryLow() {
return isMemoryLow;
super.onDestroy();
}
public static String getSpeedInfo() {

View file

@ -13,13 +13,13 @@ import java.util.regex.Pattern;
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
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 volatile boolean Running = false;
/** story hashes we need to fetch (from newly found stories) */
private static Set<String> Hashes;
static {Hashes = new HashSet<String>();}
@ -33,11 +33,15 @@ public class OriginalTextService extends SubService {
@Override
protected void exec() {
while ((Hashes.size() > 0) || (PriorityHashes.size() > 0)) {
if (parent.stopSync()) return;
gotWork();
fetchBatch(PriorityHashes);
fetchBatch(Hashes);
activelyRunning = true;
try {
while ((Hashes.size() > 0) || (PriorityHashes.size() > 0)) {
if (parent.stopSync()) return;
fetchBatch(PriorityHashes);
fetchBatch(Hashes);
}
} finally {
activelyRunning = false;
}
}
@ -97,27 +101,10 @@ public class OriginalTextService extends SubService {
return (Hashes.size() + PriorityHashes.size());
}
@Override
public boolean haveWork() {
return (getPendingCount() > 0);
}
public static void clear() {
Hashes.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;
import android.os.Process;
import android.util.Log;
import com.newsblur.activity.NbActivity;
import com.newsblur.util.AppConstants;
import com.newsblur.util.Log;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.RejectedExecutionException;
/**
* 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 {
protected NBSyncService parent;
private ExecutorService executor;
protected int startId;
private ThreadPoolExecutor executor;
private long cycleStartTime = 0L;
private SubService() {
@ -29,15 +29,13 @@ public abstract class SubService {
SubService(NBSyncService 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) {
if (parent.stopSync()) return;
parent.incrementRunningChild();
this.startId = startId;
public void start() {
Runnable r = new Runnable() {
public void run() {
if (parent.stopSync()) return;
if (NbActivity.getActiveActivityCount() < 1) {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_LESS_FAVORABLE );
} else {
@ -45,44 +43,37 @@ public abstract class SubService {
}
Thread.currentThread().setName(this.getClass().getName());
exec_();
parent.decrementRunningChild(startId);
}
};
executor.execute(r);
}
public void startConditional(int startId) {
if (haveWork()) start(startId);
}
/**
* Stub - children should implement a queue check or ready check so that startConditional()
* can more efficiently allocate threads.
*/
protected boolean haveWork() {
return true;
try {
executor.execute(r);
// enqueue a check task that will run strictly after the real one, so the callback
// can effectively check queue size to see if there are queued tasks
executor.execute(new Runnable() {
public void run() {
parent.checkCompletion();
NbActivity.updateAllActivities(NbActivity.UPDATE_STATUS);
}
});
} catch (RejectedExecutionException ree) {
// this is perfectly normal, as service soft-stop mechanics might have shut down our thread pool
// while peer subservices are still running
}
}
private synchronized void exec_() {
try {
//if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService started");
exec();
//if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService completed");
cycleStartTime = 0;
} catch (Exception e) {
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();
public void shutdown() {
if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService stopping");
Log.d(this, "SubService stopping");
executor.shutdown();
try {
executor.awaitTermination(AppConstants.SHUTDOWN_SLACK_SECONDS, TimeUnit.SECONDS);
@ -90,21 +81,18 @@ public abstract class SubService {
executor.shutdownNow();
Thread.currentThread().interrupt();
} 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) {
NbActivity.updateAllActivities(updateType);
}
protected abstract void setRunning(boolean running);
protected abstract boolean isRunning();
public 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

View file

@ -16,7 +16,7 @@ import java.util.Set;
public class UnreadsService extends SubService {
private static volatile boolean Running = false;
public static boolean activelyRunning = false;
private static volatile boolean doMetadata = false;
@ -30,17 +30,20 @@ public class UnreadsService extends SubService {
@Override
protected void exec() {
if (doMetadata) {
gotWork();
syncUnreadList();
doMetadata = false;
}
activelyRunning = true;
try {
if (doMetadata) {
syncUnreadList();
doMetadata = false;
}
if (StoryHashQueue.size() > 0) {
getNewUnreadStories();
parent.pushNotifications();
if (StoryHashQueue.size() > 0) {
getNewUnreadStories();
parent.pushNotifications();
}
} finally {
activelyRunning = false;
}
}
private void syncUnreadList() {
@ -133,7 +136,6 @@ public class UnreadsService extends SubService {
boolean isEnableNotifications = PrefsUtils.isEnableNotifications(parent);
if (! (isOfflineEnabled || isEnableNotifications)) return;
gotWork();
startExpensiveCycle();
List<String> hashBatch = new ArrayList(AppConstants.UNREAD_FETCH_BATCH_SIZE);
@ -161,8 +163,8 @@ public class UnreadsService extends SubService {
StoryHashQueue.remove(hash);
}
parent.prefetchOriginalText(response, startId);
parent.prefetchImages(response, startId);
parent.prefetchOriginalText(response);
parent.prefetchImages(response);
}
}
@ -202,17 +204,5 @@ public class UnreadsService extends SubService {
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);
c.startService(i);
}
@ -330,6 +333,14 @@ public class FeedUtils {
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) {
ReadingAction ra = ReadingAction.unshareStory(story.storyHash, story.id, story.feedId);
dbHelper.enqueueAction(ra);

View file

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

View file

@ -40,11 +40,12 @@ public class ReadingAction implements Serializable {
UNMUTE_FEEDS,
SET_NOTIFY,
INSTA_FETCH,
UPDATE_INTEL
UPDATE_INTEL,
RENAME_FEED
};
private final long time;
private final int tried;
private long time;
private int tried;
private ActionType type;
private String storyHash;
private FeedSet feedSet;
@ -59,6 +60,7 @@ public class ReadingAction implements Serializable {
private String notifyFilter;
private List<String> notifyTypes;
private Classifier classifier;
private String newFeedName;
// For mute/unmute the API call is always the active feed IDs.
// We need the feed Ids being modified for the local call.
@ -231,198 +233,34 @@ public class ReadingAction implements Serializable {
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() {
ContentValues values = new ContentValues();
values.put(DatabaseConstants.ACTION_TIME, time);
values.put(DatabaseConstants.ACTION_TRIED, tried);
values.put(DatabaseConstants.ACTION_TYPE, type.toString());
switch (type) {
case MARK_READ:
if (storyHash != null) {
values.put(DatabaseConstants.ACTION_STORY_HASH, storyHash);
} 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.");
}
// because ReadingActions will have to represent a wide and ever-growing variety of interactions,
// 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
// cardinality, only the ACTION_TIME and ACTION_TRIED values are stored in columns of their own, and
// all remaining fields are frozen as JSON, since they are never queried upon.
values.put(DatabaseConstants.ACTION_PARAMS, DatabaseConstants.JsonHelper.toJson(this));
return values;
}
public static ReadingAction fromCursor(Cursor c) {
long time = c.getLong(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_TIME));
int tried = c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_TRIED));
ReadingAction ra = new ReadingAction(time, tried);
ra.type = ActionType.valueOf(c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_TYPE)));
if (ra.type == ActionType.MARK_READ) {
String hash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
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.");
}
String params = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_PARAMS));
ReadingAction ra = DatabaseConstants.JsonHelper.fromJson(params, ReadingAction.class);
ra.time = time;
ra.tried = tried;
return ra;
}
@ -510,6 +348,10 @@ public class ReadingAction implements Serializable {
NBSyncService.addRecountCandidates(feedSet);
break;
case RENAME_FEED:
result = apiManager.renameFeed(feedId, newFeedName);
break;
default:
throw new IllegalStateException("cannot execute uknown type of action.");
@ -644,6 +486,11 @@ public class ReadingAction implements Serializable {
impact |= NbActivity.UPDATE_INTEL;
break;
case RENAME_FEED:
dbHelper.renameFeed(feedId, newFeedName);
impact |= NbActivity.UPDATE_METADATA;
break;
default:
// 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.Intent;
import android.net.Uri;
import android.os.Build;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.View;
@ -107,7 +108,10 @@ public class NewsblurWebview extends WebView {
@Override
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) {
NSLog(@" No receipt found!");
[self informError:@"No receipt found"];
return;
// return;
}
NSString *urlString = [NSString stringWithFormat:@"%@/profile/save_ios_receipt/",
@ -283,6 +283,7 @@
}];
}
#pragma mark - Table Delegate
- (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) {
options = options || {};
this.stories.each(_.bind(function(story, i) {

View file

@ -164,6 +164,9 @@ NEWSBLUR.Models.Story = Backbone.Model.extend({
detail: {background: background}
});
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) {
// console.log(['Used safari extension to open link in background', success]);
return;

View file

@ -88,7 +88,7 @@ _.extend(NEWSBLUR.ReaderFeedchooser.prototype, {
])),
(!NEWSBLUR.Globals.is_premium && $.make('div', { className: 'NB-feedchooser-info'}, [
$.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'
])
])),

View file

@ -582,7 +582,7 @@ NEWSBLUR.Views.StoryListView = Backbone.View.extend({
// 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']]);
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.reader.load_page_of_feed_stories();
}

View file

@ -34,7 +34,7 @@
float: right;">
<div class="NB-feedchooser-info">
<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>
<ul class="NB-feedchooser-premium-bullets">

View file

@ -205,6 +205,10 @@ class FacebookFetcher:
def favicon_url(self):
page_name = self.extract_page_name()
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:
picture_data = facebook_user.get_object(page_name, fields='picture')

View file

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

View file

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