Merge pull request #577 from dosiecki/master

Android: Beta 3
This commit is contained in:
Samuel Clay 2014-09-17 11:41:30 -07:00
commit a19bc9f786
14 changed files with 192 additions and 43 deletions

View file

@ -18,6 +18,10 @@
<item android:id="@+id/menu_settings"
android:title="@string/settings"
android:showAsAction="never" />
<item android:id="@+id/menu_feedback"
android:title="@string/menu_feedback"
android:showAsAction="never" />
<item android:id="@+id/menu_logout"
android:title="@string/menu_logout"

View file

@ -134,6 +134,7 @@
<string name="menu_search">Search</string>
<string name="menu_mark_all_as_read">Mark all as read</string>
<string name="menu_logout">Log out</string>
<string name="menu_feedback">Send app feedback</string>
<string name="empty_list_view_loading">Loading…</string>
<string name="empty_list_view_no_stories">No stories to read</string>

View file

@ -179,7 +179,7 @@ public abstract class ItemsList extends NbActivity implements StateChangedListen
@Override
public void storyOrderChanged(StoryOrder newValue) {
updateStoryOrderPreference(newValue);
NBSyncService.resetFeed(fs);
FeedUtils.clearReadingSession(this);
itemListFragment.resetEmptyState();
itemListFragment.hasUpdated();
itemListFragment.scrollToTop();
@ -191,7 +191,7 @@ public abstract class ItemsList extends NbActivity implements StateChangedListen
@Override
public void readFilterChanged(ReadFilter newValue) {
updateReadFilterPreference(newValue);
NBSyncService.resetFeed(fs);
FeedUtils.clearReadingSession(this);
itemListFragment.resetEmptyState();
itemListFragment.hasUpdated();
itemListFragment.scrollToTop();

View file

@ -6,7 +6,9 @@ import android.os.Bundle;
import android.preference.PreferenceManager;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.net.Uri;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@ -20,6 +22,7 @@ import com.newsblur.fragment.FolderListFragment;
import com.newsblur.fragment.LogoutDialogFragment;
import com.newsblur.service.BootReceiver;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.UIUtils;
@ -30,7 +33,6 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
private ActionBar actionBar;
private FolderListFragment folderFeedList;
private FragmentManager fragmentManager;
private Menu menu;
private TextView overlayStatusText;
private boolean isLightTheme;
private SwipeRefreshLayout swipeLayout;
@ -91,7 +93,14 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
super.onCreateOptionsMenu(menu);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main, menu);
this.menu = menu;
MenuItem feedbackItem = menu.findItem(R.id.menu_feedback);
if (AppConstants.ENABLE_FEEDBACK) {
feedbackItem.setTitle(feedbackItem.getTitle() + " (v" + PrefsUtils.getVersion(this) + ")");
} else {
feedbackItem.setVisible(false);
}
return true;
}
@ -116,6 +125,15 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
Intent settingsIntent = new Intent(this, Settings.class);
startActivity(settingsIntent);
return true;
} else if (item.getItemId() == R.id.menu_feedback) {
try {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(PrefsUtils.createFeedbackLink(this)));
startActivity(i);
} catch (Exception e) {
Log.wtf(this.getClass().getName(), "device cannot even open URLs to report feedback");
}
return true;
}
return super.onOptionsItemSelected(item);
}

View file

@ -292,16 +292,16 @@ public class BlurDatabaseHelper {
}
public void markFeedsRead(FeedSet fs, Long olderThan, Long newerThan) {
// TODO: impl at least the older/newer than cases
// split this into two steps, since the double-check feature needs to
// redo the second step separately
markFeedsRead_feedCounts(fs, olderThan, newerThan);
markFeedsRead_storyCounts(fs, olderThan, newerThan);
}
// TODO: stories
// feed counts
public void markFeedsRead_feedCounts(FeedSet fs, Long olderThan, Long newerThan) {
if (fs.isAllNormal()) {
setFeedUnreadCount(0, null, null);
} else if (fs.isAllSocial()) {
// TODO: oddly, the client never supported this before, so there is no button to invoke it. The API call
// works, though, so adding an impl. here should let us enable the button.
setSocialFeedUnreadCount(0, null, null);
} else if (fs.getMultipleFeeds() != null) {
for (String feedId : fs.getMultipleFeeds()) {
setFeedUnreadCount(0, DatabaseConstants.FEED_ID + " = ?", new String[]{feedId});
@ -311,10 +311,38 @@ public class BlurDatabaseHelper {
} else if (fs.getSingleSocialFeed() != null) {
setSocialFeedUnreadCount(0, DatabaseConstants.SOCIAL_FEED_ID + " = ?", new String[]{fs.getSingleSocialFeed().getKey()});
} else {
throw new IllegalStateException("Asked to get stories for FeedSet of unknown type.");
// TODO: fs.isAllSocial() was never supported by the UI, but the API has it, and it would be
// easy enough to add here. Should we?
throw new IllegalStateException("Asked to mark stories for FeedSet of unknown type.");
}
}
public void markFeedsRead_storyCounts(FeedSet fs, Long olderThan, Long newerThan) {
ContentValues values = new ContentValues();
values.put(DatabaseConstants.STORY_READ, true);
String rangeSelection = null;
if (olderThan != null) rangeSelection = DatabaseConstants.STORY_TIMESTAMP + " <= " + olderThan.toString();
if (newerThan != null) rangeSelection = DatabaseConstants.STORY_TIMESTAMP + " >= " + newerThan.toString();
StringBuilder feedSelection = null;
if (fs.isAllNormal()) {
// a null selection is fine for all stories
} else if (fs.getMultipleFeeds() != null) {
feedSelection = new StringBuilder(DatabaseConstants.STORY_FEED_ID + " IN ( ");
feedSelection.append(TextUtils.join(",", fs.getMultipleFeeds()));
feedSelection.append(")");
} else if (fs.getSingleFeed() != null) {
feedSelection= new StringBuilder(DatabaseConstants.STORY_FEED_ID + " = ");
feedSelection.append(fs.getSingleFeed());
} else if (fs.getSingleSocialFeed() != null) {
feedSelection= new StringBuilder(DatabaseConstants.STORY_SOCIAL_USER_ID + " = ");
feedSelection.append(fs.getSingleSocialFeed().getKey());
} else {
throw new IllegalStateException("Asked to mark stories for FeedSet of unknown type.");
}
dbRW.update(DatabaseConstants.STORY_TABLE, values, conjoinSelections(feedSelection, rangeSelection), null);
}
private void setFeedUnreadCount(int count, String whereClause, String[] whereArgs) {
ContentValues values = new ContentValues();
values.put(DatabaseConstants.FEED_NEGATIVE_COUNT, count);
@ -485,4 +513,19 @@ public class BlurDatabaseHelper {
try {c.close();} catch (Exception e) {;}
}
private static String conjoinSelections(CharSequence... args) {
StringBuilder s = null;
for (CharSequence c : args) {
if (c == null) continue;
if (s == null) {
s = new StringBuilder(c);
} else {
s.append(" AND ");
s.append(c);
}
}
if (s == null) return null;
return s.toString();
}
}

View file

@ -468,4 +468,10 @@ public class DatabaseConstants {
}
}
public static Long nullIfZero(Long l) {
if (l == null) return null;
if (l.longValue() == 0L) return null;
return l;
}
}

View file

@ -309,24 +309,24 @@ public class FeedProvider extends ContentProvider {
mdb = db;
}
public Cursor rawQuery(String sql, String[] selectionArgs) {
if (AppConstants.VERBOSE_LOG) {
if (AppConstants.VERBOSE_LOG_DB) {
Log.d(LoggingDatabase.class.getName(), "rawQuery: " + sql);
Log.d(LoggingDatabase.class.getName(), "selArgs : " + Arrays.toString(selectionArgs));
}
Cursor cursor = mdb.rawQuery(sql, selectionArgs);
if (AppConstants.VERBOSE_LOG) {
if (AppConstants.VERBOSE_LOG_DB) {
Log.d(LoggingDatabase.class.getName(), "result rows: " + cursor.getCount());
}
return cursor;
}
public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) {
if (AppConstants.VERBOSE_LOG) {
if (AppConstants.VERBOSE_LOG_DB) {
Log.d(LoggingDatabase.class.getName(), "selection: " + selection);
}
return mdb.query(table, columns, selection, selectionArgs, groupBy, having, orderBy);
}
public void execSQL(String sql) {
if (AppConstants.VERBOSE_LOG) {
if (AppConstants.VERBOSE_LOG_DB) {
Log.d(LoggingDatabase.class.getName(), "execSQL: " + sql);
}
mdb.execSQL(sql);

View file

@ -68,12 +68,15 @@ public class APIConstants {
public static final String PARAMETER_URL = "url";
public static final String PARAMETER_DAYS = "days";
public static final String PARAMETER_UPDATE_COUNTS = "update_counts";
public static final String PARAMETER_CUTOFF_TIME = "cutoff_timestamp";
public static final String PARAMETER_DIRECTION = "direction";
public static final String PARAMETER_PAGE_NUMBER = "page";
public static final String PARAMETER_ORDER = "order";
public static final String PARAMETER_READ_FILTER = "read_filter";
public static final String VALUE_ALLSOCIAL = "river:blurblogs"; // the magic value passed to the mark-read API for all social feeds
public static final String VALUE_OLDER = "older";
public static final String VALUE_NEWER = "newer";
public static final String URL_CONNECT_FACEBOOK = NEWSBLUR_URL + "/oauth/facebook_connect/";
public static final String URL_CONNECT_TWITTER = NEWSBLUR_URL + "/oauth/twitter_connect/";

View file

@ -122,7 +122,18 @@ public class APIManager {
for (String feedId : feedIds) {
values.put(APIConstants.PARAMETER_FEEDID, feedId);
}
// TODO: handle older/newer
if (includeOlder != null) {
// the app uses milliseconds but the API wants seconds
long cut = includeOlder.longValue();
values.put(APIConstants.PARAMETER_CUTOFF_TIME, Long.toString(cut/1000L));
values.put(APIConstants.PARAMETER_DIRECTION, APIConstants.VALUE_OLDER);
}
if (includeNewer != null) {
// the app uses milliseconds but the API wants seconds
long cut = includeNewer.longValue();
values.put(APIConstants.PARAMETER_CUTOFF_TIME, Long.toString(cut/1000L));
values.put(APIConstants.PARAMETER_DIRECTION, APIConstants.VALUE_NEWER);
}
APIResponse response = post(APIConstants.URL_MARK_FEED_AS_READ, values, false);
// TODO: these calls use a different return format than others: the errors field is an array, not an object

View file

@ -69,6 +69,11 @@ public class NBSyncService extends Service {
would annoy a user who is on the story list or paging through stories. */
private volatile static boolean HoldStories = false;
private volatile static boolean DoFeedsFolders = false;
private volatile static boolean isMemoryLow = false;
private volatile static boolean HaltNow = false;
private static long lastFeedCount = 0L;
private static long lastFFWriteMillis = 0L;
/** Feed sets that we need to sync and how many stories the UI wants for them. */
private static Map<FeedSet,Integer> PendingFeeds;
@ -84,8 +89,6 @@ public class NBSyncService extends Service {
private static Set<String> ImageQueue;
static { ImageQueue = new HashSet<String>(); }
private volatile static boolean HaltNow;
private PowerManager.WakeLock wl = null;
private ExecutorService executor;
private APIManager apiManager;
@ -138,7 +141,12 @@ public class NBSyncService extends Service {
*/
private synchronized void doSync(int startId) {
try {
if (HaltNow) return;
if (HaltNow) {
if (AppConstants.VERBOSE_LOG) {
Log.d(this.getClass().getName(), "skipping sync, soft interrupt set.");
}
return;
}
Log.d(this.getClass().getName(), "starting sync . . .");
@ -171,6 +179,10 @@ public class NBSyncService extends Service {
if (wl != null) wl.release();
Log.d(this.getClass().getName(), " . . . sync done");
}
if (isMemoryLow && (NbActivity.getActiveActivityCount() < 1)) {
stopSelf(startId);
}
}
/**
@ -195,8 +207,8 @@ public class NBSyncService extends Service {
if (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_MARK_READ)) == 1) {
String hash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
String feedIds = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_FEED_ID));
Long includeOlder = c.getLong(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
Long includeNewer = c.getLong(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
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) {
response = apiManager.markStoryAsRead(hash);
} else if (feedIds != null) {
@ -253,7 +265,10 @@ public class NBSyncService extends Service {
String id = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_ID));
if (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_MARK_READ)) == 1) {
String hash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
dbHelper.setStoryReadState(hash, true);
if (hash != null ) {
dbHelper.setStoryReadState(hash, true);
}
// TODO: double-check stories from feed-marks
} else if (c.getInt(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_MARK_UNREAD)) == 1) {
String hash = c.getString(c.getColumnIndexOrThrow(DatabaseConstants.ACTION_STORY_HASH));
dbHelper.setStoryReadState(hash, false);
@ -324,6 +339,8 @@ public class NBSyncService extends Service {
return;
}
long startTime = System.currentTimeMillis();
isPremium = feedResponse.isPremium;
// clean out the feed / folder tables
@ -375,6 +392,9 @@ public class NBSyncService extends Service {
// populate the starred stories count table
dbHelper.updateStarredStoriesCount(feedResponse.starredCount);
lastFFWriteMillis = System.currentTimeMillis() - startTime;
lastFeedCount = feedValues.size();
} finally {
FFSyncRunning = false;
NbActivity.updateAllActivities();
@ -561,12 +581,14 @@ public class NBSyncService extends Service {
}
public void onTrimMemory (int level) {
// if the UI is still active, definitely don't stop
if (NbActivity.getActiveActivityCount() > 0) return;
// be nice and stop if memory is even a tiny bit pressured and we aren't visible;
// the OS penalises long-running processes, and it is reasonably cheap to re-create ourself.
if (level > ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
isMemoryLow = true;
// if the UI is still active, definitely don't stop
if (NbActivity.getActiveActivityCount() > 0) return;
if (lastStartIdCompleted != -1) {
stopSelf(lastStartIdCompleted);
}
@ -642,14 +664,10 @@ public class NBSyncService extends Service {
return true;
}
/**
* Resets pagination and exhaustion flags for the given feedset, so that it can be requested fresh
* from the beginning with new parameters.
*/
public static void resetFeed(FeedSet fs) {
ExhaustedFeeds.remove(fs);
FeedPagesSeen.put(fs, 0);
FeedStoriesSeen.put(fs, 0);
public static void resetFeeds() {
ExhaustedFeeds.clear();
FeedPagesSeen.clear();
FeedStoriesSeen.clear();
}
public static void softInterrupt() {
@ -657,6 +675,10 @@ public class NBSyncService extends Service {
HaltNow = true;
}
public static void resumeFromInterrupt() {
HaltNow = false;
}
@Override
public void onDestroy() {
Log.d(this.getClass().getName(), "onDestroy");
@ -671,4 +693,14 @@ public class NBSyncService extends Service {
return null;
}
public static boolean isMemoryLow() {
return isMemoryLow;
}
public static String getSpeedInfo() {
StringBuilder s = new StringBuilder();
s.append(lastFeedCount).append(" in ").append(lastFFWriteMillis);
return s.toString();
}
}

View file

@ -5,7 +5,8 @@ public class AppConstants {
// Enables high-volume logging that may be useful for debugging. This should
// never be enabled for releases, as it not only slows down the app considerably,
// it will log sensitive info such as passwords!
public static final boolean VERBOSE_LOG = false;
public static final boolean VERBOSE_LOG = true;
public static final boolean VERBOSE_LOG_DB = false;
public static final int STATE_ALL = 0;
public static final int STATE_SOME = 1;
@ -58,4 +59,10 @@ public class AppConstants {
// how many images to prefetch before updating the countdown UI
public static final int IMAGE_PREFETCH_BATCH_SIZE = 10;
// should the feedback link be enabled (read: is this a beta?)
public static final boolean ENABLE_FEEDBACK = true;
// link to app feedback page
public static final String FEEDBACK_URL = "https://getsatisfaction.com/newsblur/topics/new?topic[style]=question&from=company&product=NewsBlur+Android+App&topic[additional_detail]=";
}

View file

@ -117,6 +117,7 @@ public class FeedUtils {
} catch (Exception e) {
; // this one call can evade the on-upgrade DB wipe and throw exceptions
}
NBSyncService.resetFeeds();
return null;
}
}.execute();

View file

@ -19,6 +19,7 @@ import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.util.Log;
import com.newsblur.R;
@ -30,6 +31,7 @@ import com.newsblur.service.NBSyncService;
public class PrefsUtils {
public static void saveLogin(final Context context, final String userName, final String cookie) {
NBSyncService.resumeFromInterrupt();
final SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
final Editor edit = preferences.edit();
edit.putString(PrefConstants.PREF_COOKIE, cookie);
@ -45,10 +47,8 @@ public class PrefsUtils {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
String version;
try {
version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
} catch (NameNotFoundException nnfe) {
String version = getVersion(context);
if (version == null) {
Log.w(PrefsUtils.class.getName(), "could not determine app version");
return;
}
@ -72,6 +72,26 @@ public class PrefsUtils {
}
public static String getVersion(Context context) {
try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
} catch (NameNotFoundException nnfe) {
Log.w(PrefsUtils.class.getName(), "could not determine app version");
return null;
}
}
public static String createFeedbackLink(Context context) {
StringBuilder s = new StringBuilder(AppConstants.FEEDBACK_URL);
s.append("<give us some feedback!>%0A%0A");
s.append("%0Aapp version: ").append(getVersion(context));
s.append("%0Aandroid version: ").append(Build.VERSION.RELEASE);
s.append("%0Adevice: ").append(Build.MANUFACTURER + "+" + Build.MODEL + "+(" + Build.BOARD + ")");
s.append("%0Amemory: ").append(NBSyncService.isMemoryLow() ? "low" : "normal");
s.append("%0Aspeed: ").append(NBSyncService.getSpeedInfo());
return s.toString();
}
public static void logout(Context context) {
NBSyncService.softInterrupt();

View file

@ -70,9 +70,12 @@ public class SocialItemViewBinder implements ViewBinder {
return true;
} else if (TextUtils.equals(columnName, DatabaseConstants.STORY_AUTHORS)) {
String authors = cursor.getString(columnIndex);
if (!TextUtils.isEmpty(authors)) {
((TextView) view).setText(authors.toUpperCase());
}
if (TextUtils.isEmpty(authors)) {
view.setVisibility(View.GONE);
} else {
((TextView) view).setText(authors.toUpperCase());
view.setVisibility(View.VISIBLE);
}
return true;
} else if (TextUtils.equals(columnName, DatabaseConstants.STORY_TITLE)) {
((TextView) view).setText(Html.fromHtml(cursor.getString(columnIndex)));