diff --git a/clients/android/NewsBlur/res/menu/main.xml b/clients/android/NewsBlur/res/menu/main.xml index eabb024ae..2152bac04 100644 --- a/clients/android/NewsBlur/res/menu/main.xml +++ b/clients/android/NewsBlur/res/menu/main.xml @@ -24,7 +24,17 @@ + android:showAsAction="never" > + + + + + + Mark all as read Log out Send app feedback + Create a support post + Email a bug report Login as... Login As User diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/Main.java b/clients/android/NewsBlur/src/com/newsblur/activity/Main.java index f5e13e7d1..9794291e7 100644 --- a/clients/android/NewsBlur/src/com/newsblur/activity/Main.java +++ b/clients/android/NewsBlur/src/com/newsblur/activity/Main.java @@ -238,7 +238,10 @@ 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) { + } else if (item.getItemId() == R.id.menu_feedback_email) { + PrefsUtils.sendLogEmail(this); + return true; + } else if (item.getItemId() == R.id.menu_feedback_post) { try { Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(PrefsUtils.createFeedbackLink(this))); diff --git a/clients/android/NewsBlur/src/com/newsblur/activity/NbActivity.java b/clients/android/NewsBlur/src/com/newsblur/activity/NbActivity.java index d62cc138f..711e0ca51 100644 --- a/clients/android/NewsBlur/src/com/newsblur/activity/NbActivity.java +++ b/clients/android/NewsBlur/src/com/newsblur/activity/NbActivity.java @@ -59,7 +59,7 @@ public class NbActivity extends Activity { @Override protected void onResume() { - if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onResume"); + com.newsblur.util.Log.d(this.getClass().getName(), "onResume" + UIUtils.getMemoryUsageDebug(this)); super.onResume(); finishIfNotLoggedIn(); @@ -81,7 +81,7 @@ public class NbActivity extends Activity { protected void finishIfNotLoggedIn() { String currentLoginKey = PrefsUtils.getUniqueLoginKey(this); if(currentLoginKey == null || !currentLoginKey.equals(uniqueLoginKey)) { - Log.d( this.getClass().getName(), "This activity was for a different login. finishing it."); + com.newsblur.util.Log.d( this.getClass().getName(), "This activity was for a different login. finishing it."); finish(); } } diff --git a/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java b/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java index c75b6fa39..27b216757 100644 --- a/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java +++ b/clients/android/NewsBlur/src/com/newsblur/database/BlurDatabaseHelper.java @@ -56,7 +56,7 @@ public class BlurDatabaseHelper { private SQLiteDatabase dbRW; public BlurDatabaseHelper(Context context) { - if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "new DB conn requested"); + com.newsblur.util.Log.d(this.getClass().getName(), "new DB conn requested"); this.context = context; synchronized (RW_MUTEX) { dbWrapper = new BlurDatabase(context); @@ -78,7 +78,9 @@ public class BlurDatabaseHelper { } public void dropAndRecreateTables() { + com.newsblur.util.Log.i(this.getClass().getName(), "dropping and recreating all tables . . ."); synchronized (RW_MUTEX) {dbWrapper.dropAndRecreateTables();} + com.newsblur.util.Log.i(this.getClass().getName(), ". . . tables recreated."); } public String getEngineVersion() { diff --git a/clients/android/NewsBlur/src/com/newsblur/fragment/ItemListFragment.java b/clients/android/NewsBlur/src/com/newsblur/fragment/ItemListFragment.java index f4d8fab84..6e5e81c78 100644 --- a/clients/android/NewsBlur/src/com/newsblur/fragment/ItemListFragment.java +++ b/clients/android/NewsBlur/src/com/newsblur/fragment/ItemListFragment.java @@ -279,10 +279,12 @@ public abstract class ItemListFragment extends NbFragment implements OnScrollLis if (cursor != null) { if (NBSyncService.ResetSession) { // the DB hasn't caught up yet from the last story list; don't display stale stories. + com.newsblur.util.Log.i(this.getClass().getName(), "discarding stale load"); triggerRefresh(1, 0); return; } cursorSeenYet = true; + com.newsblur.util.Log.d(this.getClass().getName(), "loaded cursor with count: " + cursor.getCount()); if (cursor.getCount() < 1) { triggerRefresh(1, 0); } diff --git a/clients/android/NewsBlur/src/com/newsblur/network/APIManager.java b/clients/android/NewsBlur/src/com/newsblur/network/APIManager.java index 1abe5133d..129b8c02c 100644 --- a/clients/android/NewsBlur/src/com/newsblur/network/APIManager.java +++ b/clients/android/NewsBlur/src/com/newsblur/network/APIManager.java @@ -95,7 +95,7 @@ public class APIManager { public LoginResponse login(final String username, final String password) { // This call should be pretty rare, but is expensive on the server side. Log it // at an above-debug level so it will be noticed if it ever gets called too often. - Log.i(this.getClass().getName(), "calling login API"); + com.newsblur.util.Log.i(this.getClass().getName(), "calling login API"); final ContentValues values = new ContentValues(); values.put(APIConstants.PARAMETER_USERNAME, username); values.put(APIConstants.PARAMETER_PASSWORD, password); @@ -111,7 +111,7 @@ public class APIManager { ContentValues values = new ContentValues(); values.put(APIConstants.PARAMETER_USER, username); String urlString = buildUrl(APIConstants.PATH_LOGINAS) + "?" + builderGetParametersString(values); - Log.i(this.getClass().getName(), "doing superuser swap: " + urlString); + com.newsblur.util.Log.i(this.getClass().getName(), "doing superuser swap: " + urlString); // This API returns a redirect that means the call worked, but we do not want to follow it. To // just get the cookie from the 302 and stop, we directly use a one-off OkHttpClient. Request.Builder requestBuilder = new Request.Builder().url(urlString); @@ -692,13 +692,13 @@ public class APIManager { */ private void backoffSleep(int tryCount) { if (tryCount == 0) return; - Log.i(this.getClass().getName(), "API call failed, pausing before retry number " + tryCount); + com.newsblur.util.Log.i(this.getClass().getName(), "API call failed, pausing before retry number " + tryCount); try { // simply double the base sleep time for each subsequent try long factor = Math.round(Math.pow(2.0d, tryCount)); Thread.sleep(AppConstants.API_BACKOFF_BASE_MILLIS * factor); } catch (InterruptedException ie) { - Log.w(this.getClass().getName(), "Abandoning API backoff due to interrupt."); + com.newsblur.util.Log.w(this.getClass().getName(), "Abandoning API backoff due to interrupt."); } } diff --git a/clients/android/NewsBlur/src/com/newsblur/network/APIResponse.java b/clients/android/NewsBlur/src/com/newsblur/network/APIResponse.java index 1771bc6e4..3c65af34b 100644 --- a/clients/android/NewsBlur/src/com/newsblur/network/APIResponse.java +++ b/clients/android/NewsBlur/src/com/newsblur/network/APIResponse.java @@ -54,7 +54,7 @@ public class APIResponse { this.responseCode = response.code(); if (responseCode != expectedReturnCode) { - Log.e(this.getClass().getName(), "API returned error code " + response.code() + " calling " + request.url().toString() + " - expected " + expectedReturnCode); + com.newsblur.util.Log.e(this.getClass().getName(), "API returned error code " + response.code() + " calling " + request.url().toString() + " - expected " + expectedReturnCode); this.isError = true; return; } @@ -66,7 +66,7 @@ public class APIResponse { this.responseBody = response.body().string(); readTime = System.currentTimeMillis() - startTime; } catch (Exception e) { - Log.e(this.getClass().getName(), e.getClass().getName() + " (" + e.getMessage() + ") reading " + request.url().toString(), e); + com.newsblur.util.Log.e(this.getClass().getName(), e.getClass().getName() + " (" + e.getMessage() + ") reading " + request.url().toString(), e); this.isError = true; return; } @@ -83,12 +83,10 @@ public class APIResponse { } } - if (AppConstants.VERBOSE_LOG_NET) { - Log.d(this.getClass().getName(), String.format("called %s in %dms and %dms to read %dB", request.url().toString(), connectTime, readTime, responseBody.length())); - } + com.newsblur.util.Log.d(this.getClass().getName(), String.format("called %s in %dms and %dms to read %dB", request.url().toString(), connectTime, readTime, responseBody.length())); } catch (IOException ioe) { - Log.e(this.getClass().getName(), "Error (" + ioe.getMessage() + ") calling " + request.url().toString(), ioe); + com.newsblur.util.Log.e(this.getClass().getName(), "Error (" + ioe.getMessage() + ") calling " + request.url().toString(), ioe); this.isError = true; return; } @@ -98,6 +96,7 @@ public class APIResponse { * Construct and empty/offline response. Signals that the call was not made. */ public APIResponse(Context context) { + com.newsblur.util.Log.w(this.getClass().getName(), "failing an offline API response"); this.isError = true; } diff --git a/clients/android/NewsBlur/src/com/newsblur/network/domain/NewsBlurResponse.java b/clients/android/NewsBlur/src/com/newsblur/network/domain/NewsBlurResponse.java index 75a0150e9..26b84ae81 100644 --- a/clients/android/NewsBlur/src/com/newsblur/network/domain/NewsBlurResponse.java +++ b/clients/android/NewsBlur/src/com/newsblur/network/domain/NewsBlurResponse.java @@ -20,11 +20,11 @@ public class NewsBlurResponse { public boolean isError() { if (isProtocolError) return true; if ((message != null) && (!message.equals(""))) { - Log.d(this.getClass().getName(), "Response interpreted as error due to 'message' field: " + message); + com.newsblur.util.Log.d(this.getClass().getName(), "Response interpreted as error due to 'message' field: " + message); return true; } if ((errors != null) && (errors.length > 0) && (errors[0] != null)) { - Log.d(this.getClass().getName(), "Response interpreted as error due to 'errors' field: " + errors[0]); + com.newsblur.util.Log.d(this.getClass().getName(), "Response interpreted as error due to 'errors' field: " + errors[0]); return true; } return false; diff --git a/clients/android/NewsBlur/src/com/newsblur/service/CleanupService.java b/clients/android/NewsBlur/src/com/newsblur/service/CleanupService.java index 8be2d7845..b618a5bad 100644 --- a/clients/android/NewsBlur/src/com/newsblur/service/CleanupService.java +++ b/clients/android/NewsBlur/src/com/newsblur/service/CleanupService.java @@ -20,24 +20,24 @@ public class CleanupService extends SubService { gotWork(); - if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "cleaning up old stories"); + com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up old stories"); parent.dbHelper.cleanupVeryOldStories(); if (!PrefsUtils.isKeepOldStories(parent)) { parent.dbHelper.cleanupReadStories(); } - if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "cleaning up old story texts"); + com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up old story texts"); parent.dbHelper.cleanupStoryText(); - if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "cleaning up story image cache"); + com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up story image cache"); FileCache imageCache = FileCache.asStoryImageCache(parent); imageCache.cleanupUnusedOrOld(parent.dbHelper.getAllStoryImages()); - if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "cleaning up icon cache"); + com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up icon cache"); FileCache iconCache = FileCache.asIconCache(parent); iconCache.cleanupOld(); - if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "cleaning up thumbnail cache"); + com.newsblur.util.Log.d(this.getClass().getName(), "cleaning up thumbnail cache"); FileCache thumbCache = FileCache.asThumbnailCache(parent); thumbCache.cleanupUnusedOrOld(parent.dbHelper.getAllStoryThumbnails()); diff --git a/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java b/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java index 293ae345a..3e45d433b 100644 --- a/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java +++ b/clients/android/NewsBlur/src/com/newsblur/service/NBSyncService.java @@ -169,6 +169,7 @@ public class NBSyncService extends Service { originalTextService = new OriginalTextService(this); unreadsService = new UnreadsService(this); imagePrefetchService = new ImagePrefetchService(this); + com.newsblur.util.Log.offerContext(this); } } @@ -261,7 +262,7 @@ public class NBSyncService extends Service { if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "finishing primary sync"); } catch (Exception e) { - Log.e(this.getClass().getName(), "Sync error.", e); + com.newsblur.util.Log.e(this.getClass().getName(), "Sync error.", e); } finally { decrementRunningChild(startId); } @@ -296,9 +297,9 @@ public class NBSyncService extends Service { if (upgraded || autoVac) { HousekeepingRunning = true; NbActivity.updateAllActivities(NbActivity.UPDATE_STATUS); - Log.i(this.getClass().getName(), "rebuilding DB . . ."); + com.newsblur.util.Log.i(this.getClass().getName(), "rebuilding DB . . ."); dbHelper.vacuum(); - Log.i(this.getClass().getName(), ". . . . done rebuilding DB"); + com.newsblur.util.Log.i(this.getClass().getName(), ". . . . done rebuilding DB"); PrefsUtils.updateLastVacuumTime(this); } } finally { @@ -331,7 +332,7 @@ public class NBSyncService extends Service { try { ra = ReadingAction.fromCursor(c); } catch (IllegalArgumentException e) { - Log.e(this.getClass().getName(), "error unfreezing ReadingAction", e); + com.newsblur.util.Log.e(this.getClass().getName(), "error unfreezing ReadingAction", e); dbHelper.clearAction(id); continue actionsloop; } @@ -339,20 +340,20 @@ public class NBSyncService extends Service { // don't block story loading unless this is a brand new action if ((ra.getTried() > 0) && (PendingFeed != null)) continue actionsloop; - if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "attempting action: " + ra.toContentValues().toString()); + com.newsblur.util.Log.d(this.getClass().getName(), "attempting action: " + ra.toContentValues().toString()); NewsBlurResponse response = ra.doRemote(apiManager); if (response == null) { - Log.e(this.getClass().getName(), "Discarding reading action with client-side error."); + com.newsblur.util.Log.e(this.getClass().getName(), "Discarding reading action with client-side error."); dbHelper.clearAction(id); } else if (response.isProtocolError) { // the network failed or we got a non-200, so be sure we retry - Log.i(this.getClass().getName(), "Holding reading action with server-side or network error."); + com.newsblur.util.Log.i(this.getClass().getName(), "Holding reading action with server-side or network error."); dbHelper.incrementActionTried(id); noteHardAPIFailure(); continue actionsloop; } else if (response.isError()) { - Log.e(this.getClass().getName(), "Discarding reading action with user error."); + com.newsblur.util.Log.e(this.getClass().getName(), "Discarding reading action with user error."); dbHelper.clearAction(id); String message = response.getErrorMessage(null); if (message != null) NbActivity.toastError(message); @@ -413,6 +414,8 @@ public class NBSyncService extends Service { return; } + com.newsblur.util.Log.i(this.getClass().getName(), "ready to sync feed list"); + FFSyncRunning = true; NbActivity.updateAllActivities(NbActivity.UPDATE_STATUS); @@ -434,7 +437,7 @@ public class NBSyncService extends Service { // we should not have got this far without being logged in, so the server either // expired or ignored out cookie. keep track of this. isAuth = false; - Log.w(this.getClass().getName(), "Server ignored or rejected auth cookie."); + com.newsblur.util.Log.w(this.getClass().getName(), "Server ignored or rejected auth cookie."); DoFeedsFolders = true; noteHardAPIFailure(); return; @@ -528,6 +531,8 @@ public class NBSyncService extends Service { lastFFWriteMillis = System.currentTimeMillis() - startTime; lastFeedCount = feedValues.size(); + com.newsblur.util.Log.i(this.getClass().getName(), "got feed list: " + getSpeedInfo()); + UnreadsService.doMetadata(); unreadsService.start(startId); cleanupService.start(startId); @@ -564,6 +569,8 @@ public class NBSyncService extends Service { return; } + com.newsblur.util.Log.i(this.getClass().getName(), "recounting dirty feed sets: " + dirtySets.size()); + // if we are offline, the best we can do is perform a local unread recount and // save the true one for when we go back online. if (!NetworkUtils.isOnline(this)) { @@ -582,7 +589,7 @@ public class NBSyncService extends Service { UnreadCountResponse apiResponse = apiManager.getFeedUnreadCounts(apiIds); if ((apiResponse == null) || (apiResponse.isError())) { - Log.w(this.getClass().getName(), "Bad response to feed_unread_count"); + com.newsblur.util.Log.w(this.getClass().getName(), "Bad response to feed_unread_count"); return; } if (apiResponse.feeds != null ) { @@ -633,7 +640,7 @@ public class NBSyncService extends Service { } try { if (ExhaustedFeeds.contains(fs)) { - Log.i(this.getClass().getName(), "No more stories for feed set: " + fs); + com.newsblur.util.Log.i(this.getClass().getName(), "No more stories for feed set: " + fs); finished = true; return; } @@ -719,11 +726,11 @@ public class NBSyncService extends Service { private boolean isStoryResponseGood(StoriesResponse response) { if (response == null) { - Log.e(this.getClass().getName(), "Null response received while loading stories."); + com.newsblur.util.Log.e(this.getClass().getName(), "Null response received while loading stories."); return false; } if (response.stories == null) { - Log.e(this.getClass().getName(), "Null stories member received while loading stories."); + com.newsblur.util.Log.e(this.getClass().getName(), "Null stories member received while loading stories."); return false; } return true; @@ -772,10 +779,12 @@ public class NBSyncService extends Service { } } + com.newsblur.util.Log.d(NBSyncService.class.getName(), "got stories from main fetch loop: " + apiResponse.stories.length); dbHelper.insertStories(apiResponse, true); } void insertStories(StoriesResponse apiResponse) { + com.newsblur.util.Log.d(NBSyncService.class.getName(), "got stories from sub sync: " + apiResponse.stories.length); dbHelper.insertStories(apiResponse, false); } @@ -818,7 +827,7 @@ public class NBSyncService extends Service { static boolean stopSync(Context context) { if (HaltNow) { - if (AppConstants.VERBOSE_LOG) Log.d(NBSyncService.class.getName(), "stopping sync, soft interrupt set."); + com.newsblur.util.Log.i(NBSyncService.class.getName(), "stopping sync, soft interrupt set."); return true; } if (context == null) return false; @@ -834,13 +843,14 @@ public class NBSyncService extends Service { } private void noteHardAPIFailure() { + com.newsblur.util.Log.w(this.getClass().getName(), "hard API failure"); lastAPIFailure = System.currentTimeMillis(); } private boolean backoffBackgroundCalls() { if (NbActivity.getActiveActivityCount() > 0) return false; if (System.currentTimeMillis() > (lastAPIFailure + AppConstants.API_BACKGROUND_BACKOFF_MILLIS)) return false; - Log.i(this.getClass().getName(), "abandoning background sync due to recent API failures."); + com.newsblur.util.Log.i(this.getClass().getName(), "abandoning background sync due to recent API failures."); return true; } @@ -927,7 +937,7 @@ public class NBSyncService extends Service { */ public static boolean requestMoreForFeed(FeedSet fs, int desiredStoryCount, int callerSeen) { if (ExhaustedFeeds.contains(fs)) { - if (AppConstants.VERBOSE_LOG) Log.i(NBSyncService.class.getName(), "rejecting request for feedset that is exhaused"); + com.newsblur.util.Log.d(NBSyncService.class.getName(), "rejecting request for feedset that is exhaused"); return false; } @@ -963,6 +973,7 @@ public class NBSyncService extends Service { * session gets cleared before the next one is populated. */ public static void resetReadingSession() { + com.newsblur.util.Log.d(NBSyncService.class.getName(), "requesting reading session reset"); synchronized (PENDING_FEED_MUTEX) { PendingFeed = null; ResetSession = true; @@ -973,7 +984,7 @@ public class NBSyncService extends Service { * Reset the API pagniation state for the given feedset, presumably because the order or filter changed. */ public static void resetFetchState(FeedSet fs) { - Log.d(NBSyncService.class.getName(), "requesting feed fetch state reset"); + com.newsblur.util.Log.d(NBSyncService.class.getName(), "requesting feed fetch state reset"); ResetFeed = fs; } @@ -996,7 +1007,7 @@ public class NBSyncService extends Service { } public static void softInterrupt() { - if (AppConstants.VERBOSE_LOG) Log.d(NBSyncService.class.getName(), "soft stop"); + com.newsblur.util.Log.i(NBSyncService.class.getName(), "soft stop"); HaltNow = true; } @@ -1041,7 +1052,7 @@ public class NBSyncService extends Service { if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "onDestroy - execution halted"); super.onDestroy(); } catch (Exception ex) { - Log.e(this.getClass().getName(), "unclean shutdown", ex); + com.newsblur.util.Log.e(this.getClass().getName(), "unclean shutdown", ex); } } diff --git a/clients/android/NewsBlur/src/com/newsblur/service/SubService.java b/clients/android/NewsBlur/src/com/newsblur/service/SubService.java index a134c26c4..430d3f3fc 100644 --- a/clients/android/NewsBlur/src/com/newsblur/service/SubService.java +++ b/clients/android/NewsBlur/src/com/newsblur/service/SubService.java @@ -58,7 +58,7 @@ public abstract class SubService { //if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "SubService completed"); cycleStartTime = 0; } catch (Exception e) { - Log.e(this.getClass().getName(), "Sync error.", e); + com.newsblur.util.Log.e(this.getClass().getName(), "Sync error.", e); } finally { if (isRunning()) { setRunning(false); @@ -116,7 +116,7 @@ public abstract class SubService { if (cooloffTimeMs > AppConstants.DUTY_CYCLE_BACKOFF_CAP_MILLIS) cooloffTimeMs = AppConstants.DUTY_CYCLE_BACKOFF_CAP_MILLIS; if (NbActivity.getActiveActivityCount() > 0 ) { - if (AppConstants.VERBOSE_LOG) Log.d(this.getClass().getName(), "Sleeping for : " + cooloffTimeMs + "ms to enforce max duty cycle."); + com.newsblur.util.Log.d(this.getClass().getName(), "Sleeping for : " + cooloffTimeMs + "ms to enforce max duty cycle."); try { Thread.sleep(cooloffTimeMs); } catch (InterruptedException e) { diff --git a/clients/android/NewsBlur/src/com/newsblur/service/UnreadsService.java b/clients/android/NewsBlur/src/com/newsblur/service/UnreadsService.java index 0afaf8f11..33819eb32 100644 --- a/clients/android/NewsBlur/src/com/newsblur/service/UnreadsService.java +++ b/clients/android/NewsBlur/src/com/newsblur/service/UnreadsService.java @@ -61,6 +61,7 @@ public class UnreadsService extends SubService { // the set of unreads from the API, we will mark them as read. note that this collection // will be searched many times for new unreads, so it should be a Set, not a List. Set oldUnreadHashes = parent.dbHelper.getUnreadStoryHashesAsSet(); + com.newsblur.util.Log.i(this.getClass().getName(), "starting unread count: " + oldUnreadHashes.size()); // a place to store and then sort unread hashes we aim to fetch. note the member format // is made to match the format of the API response (a list of [hash, date] tuples). it @@ -69,6 +70,7 @@ public class UnreadsService extends SubService { // process the api response, both bookkeeping no-longer-unread stories and populating // the sortation list we will use to create the fetch list for step two + int count = 0; feedloop: for (Entry> entry : unreadHashes.unreadHashes.entrySet()) { // the API gives us a list of unreads, split up by feed ID. the unreads are tuples of // story hash and date @@ -84,8 +86,12 @@ public class UnreadsService extends SubService { } else { oldUnreadHashes.remove(newUnread[0]); } + count++; } } + com.newsblur.util.Log.i(this.getClass().getName(), "new unread count: " + count); + com.newsblur.util.Log.i(this.getClass().getName(), "new unreads found: " + sortationList.size()); + com.newsblur.util.Log.i(this.getClass().getName(), "unreads to retire: " + oldUnreadHashes.size()); // now sort the unreads we need to fetch so they are fetched roughly in the order // the user is likely to read them. if the user reads newest first, those come first. diff --git a/clients/android/NewsBlur/src/com/newsblur/util/FeedUtils.java b/clients/android/NewsBlur/src/com/newsblur/util/FeedUtils.java index a4a756088..d8ff8b4ce 100644 --- a/clients/android/NewsBlur/src/com/newsblur/util/FeedUtils.java +++ b/clients/android/NewsBlur/src/com/newsblur/util/FeedUtils.java @@ -143,7 +143,13 @@ public class FeedUtils { } private static void setStoryReadState(Story story, Context context, boolean read) { - dbHelper.touchStory(story.storyHash); + try { + // this shouldn't throw errors, but crash logs suggest something is racing it for DB resources. + // capture logs in hopes of finding the correlated action + dbHelper.touchStory(story.storyHash); + } catch (Exception e) { + com.newsblur.util.Log.e(FeedUtils.class.getName(), "error touching story state in DB", e); + } if (story.read == read) { return; } // tell the sync service we need to mark read diff --git a/clients/android/NewsBlur/src/com/newsblur/util/Log.java b/clients/android/NewsBlur/src/com/newsblur/util/Log.java new file mode 100644 index 000000000..51425401a --- /dev/null +++ b/clients/android/NewsBlur/src/com/newsblur/util/Log.java @@ -0,0 +1,135 @@ +package com.newsblur.util; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; + +import android.content.Context; + +/** + * A low-overhead, fail-fast, dependency-free, file-backed log collector. This utility + * will persist debug messages in a file in such a way that sacrifices any guarantees + * in order to have as few side-effects as possible. The resulting log file will have + * as many of the messages sent here as possible, but should not be expected to have + * all of them. The file is trimmed down to a set number of lines if it grows too big. + */ +public class Log { + + private static final String D = "DEBUG "; + private static final String I = "INFO "; + private static final String W = "WARN "; + private static final String E = "ERROR "; + + private static final String LOG_NAME_INTERNAL = "logbuffer.txt"; + private static final int MAX_LINE_SIZE = 4 * 1024; + private static final int TRIM_LINES = 256; // trim the log down to 256 lines + private static final long MAX_SIZE = 512L * MAX_LINE_SIZE; // when it is at least 512 lines long + + private static Queue q; + private static ExecutorService executor; + private static File logloc = null; + static { + q = new ConcurrentLinkedQueue(); + executor = Executors.newFixedThreadPool(1); + } + + private Log() {} // util class - no instances + + public static void d(String tag, String m) { + if (AppConstants.VERBOSE_LOG) android.util.Log.d(tag, m); + add(D, tag, m, null); + } + + public static void i(String tag, String m) { + android.util.Log.i(tag, m); + add(I, tag, m, null); + } + + public static void w(String tag, String m) { + android.util.Log.w(tag, m); + add(W, tag, m, null); + } + + public static void e(String tag, String m) { + android.util.Log.e(tag, m); + add(E, tag, m, null); + } + + public static void e(String tag, String m, Throwable t) { + android.util.Log.e(tag, m, t); + add(E, tag, m, t); + } + + private static void add(String lvl, String tag, String m, Throwable t) { + if (q.size() > TRIM_LINES) return; + if (m != null && m.length() > MAX_LINE_SIZE) m = m.substring(0, MAX_LINE_SIZE); + StringBuilder s = new StringBuilder(); + s.append(Long.toString(System.currentTimeMillis())) + .append(" ") + .append(lvl) + .append(tag) + .append(" "); + if (t != null) { + s.append(t.getMessage()); + s.append(" "); + } + s.append(m); + q.offer(s.toString()); + Runnable r = new Runnable() { + public void run() { + proc(); + } + }; + executor.execute(r); + } + + public static void offerContext(Context c) { + logloc = c.getExternalCacheDir(); + } + + private static void proc() { + synchronized (q) { + if (logloc == null) return; // not yet spun up + String line = q.poll(); + if (line == null) return; + File f = new File(logloc, LOG_NAME_INTERNAL); + try (BufferedWriter w = new BufferedWriter(new FileWriter(f, true))) { + w.append(line); + w.newLine(); + } catch (Throwable t) { + ; // explicitly do nothing, log nothing, and fail fast. this is a utility to + // provice as much info as possible while having absolute minimal impact or + // side effect on performance or app operation + } + if (f.length() < MAX_SIZE) return; + android.util.Log.i(Log.class.getName(), "trimming"); + List lines = new ArrayList(TRIM_LINES * 2); + try (BufferedReader r = new BufferedReader(new FileReader(f))) { + for (String l = r.readLine(); l != null; l = r.readLine()) { + lines.add(l); + } + } catch (Throwable t) {;} + int offset = lines.size() - TRIM_LINES; + try (BufferedWriter w = new BufferedWriter(new FileWriter(f, false))) { + for (int i = offset; i < lines.size(); i++) { + w.append(lines.get(i)); + w.newLine(); + } + } catch (Throwable t) {;} + } + } + + public static File getLogfile() { + return new File(logloc, LOG_NAME_INTERNAL); + } + +} + diff --git a/clients/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java b/clients/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java index a1bf38d8d..5e662d406 100644 --- a/clients/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java +++ b/clients/android/NewsBlur/src/com/newsblur/util/PrefsUtils.java @@ -62,7 +62,7 @@ public class PrefsUtils { String oldVersion = prefs.getString(AppConstants.LAST_APP_VERSION, null); if ( (oldVersion == null) || (!oldVersion.equals(version)) ) { - Log.i(PrefsUtils.class.getName(), "detected new version of app:" + version); + com.newsblur.util.Log.i(PrefsUtils.class.getName(), "detected new version of app:" + version); return true; } return false; @@ -88,17 +88,48 @@ public class PrefsUtils { public static String createFeedbackLink(Context context) { StringBuilder s = new StringBuilder(AppConstants.FEEDBACK_URL); - s.append("%0A%0A"); - s.append("%0Aapp version: ").append(getVersion(context)); - s.append("%0Aandroid version: ").append(Build.VERSION.RELEASE).append(" (" + Build.DISPLAY + ")"); - s.append("%0Adevice: ").append(Build.MANUFACTURER + "+" + Build.MODEL + "+(" + Build.BOARD + ")"); - s.append("%0Asqlite version: ").append(FeedUtils.dbHelper.getEngineVersion()); - s.append("%0Ausername: ").append(getUserDetails(context).username); - s.append("%0Aserver: ").append(APIConstants.isCustomServer() ? "default" : "custom"); - s.append("%0Amemory: ").append(NBSyncService.isMemoryLow() ? "low" : "normal"); - s.append("%0Aspeed: ").append(NBSyncService.getSpeedInfo()); - s.append("%0Apending actions: ").append(NBSyncService.getPendingInfo()); - s.append("%0Apremium: "); + s.append("%0A%0A%0A"); + String info = getDebugInfo(context); + s.append(info.replace("\n", "%0A")); + return s.toString(); + } + + public static void sendLogEmail(Context context) { + File f = com.newsblur.util.Log.getLogfile(); + if (f == null) return; + android.net.Uri localPath = android.net.Uri.fromFile(f); + Intent i = new Intent(Intent.ACTION_SEND); + i.setType("*/*"); + i.putExtra(Intent.EXTRA_EMAIL, new String[]{"android@newsblur.com"}); + i.putExtra(Intent.EXTRA_SUBJECT, "Android logs"); + i.putExtra(Intent.EXTRA_TEXT, getDebugInfo(context)); + i.putExtra(Intent.EXTRA_STREAM, localPath); + if (i.resolveActivity(context.getPackageManager()) != null) { + context.startActivity(i); + } + } + + private static String getDebugInfo(Context context) { + StringBuilder s = new StringBuilder(); + s.append("app version: ").append(getVersion(context)); + s.append("\n"); + s.append("android version: ").append(Build.VERSION.RELEASE).append(" (" + Build.DISPLAY + ")"); + s.append("\n"); + s.append("device: ").append(Build.MANUFACTURER + "+" + Build.MODEL + "+(" + Build.BOARD + ")"); + s.append("\n"); + s.append("sqlite version: ").append(FeedUtils.dbHelper.getEngineVersion()); + s.append("\n"); + s.append("username: ").append(getUserDetails(context).username); + 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()); + s.append("\n"); + s.append("premium: "); if (NBSyncService.isPremium == Boolean.TRUE) { s.append("yes"); } else if (NBSyncService.isPremium == Boolean.FALSE) { @@ -106,9 +137,13 @@ public class PrefsUtils { } else { s.append("unknown"); } - s.append("%0Aprefetch: ").append(isOfflineEnabled(context) ? "yes" : "no"); - s.append("%0Akeepread: ").append(isKeepOldStories(context) ? "yes" : "no"); - s.append("%0Athumbs: ").append(isShowThumbnails(context) ? "yes" : "no"); + s.append("\n"); + s.append("prefetch: ").append(isOfflineEnabled(context) ? "yes" : "no"); + s.append("\n"); + s.append("keepread: ").append(isKeepOldStories(context) ? "yes" : "no"); + s.append("\n"); + s.append("thumbs: ").append(isShowThumbnails(context) ? "yes" : "no"); + s.append("\n"); return s.toString(); }