mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-09-18 21:50:56 +00:00
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:
commit
4a9d604293
62 changed files with 1336 additions and 1082 deletions
|
@ -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:
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
17
clients/android/NewsBlur/res/layout/dialog_rename_feed.xml
Normal file
17
clients/android/NewsBlur/res/layout/dialog_rename_feed.xml
Normal 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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
63
clients/android/NewsBlur/res/menu/story_context.xml
Normal file
63
clients/android/NewsBlur/res/menu/story_context.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {;}
|
||||
|
|
|
@ -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 + " (" +
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'
|
||||
])
|
||||
])),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue