Android Open Source - ghwatch Unread Notifications Service






From Project

Back to project page ghwatch.

License

The source code is released under:

Apache License

If you think the Android project ghwatch listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * Copyright 2014 contributors as indicated by the @authors tag.
 * //w ww. ja  v  a 2 s  .  c  o  m
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.daskiworks.ghwatch.backend;

import java.io.File;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.net.NoRouteToHostException;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

import org.apache.http.auth.AuthenticationException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.daskiworks.ghwatch.ActivityTracker;
import com.daskiworks.ghwatch.MainActivity;
import com.daskiworks.ghwatch.R;
import com.daskiworks.ghwatch.UnreadAppWidgetProvider;
import com.daskiworks.ghwatch.Utils;
import com.daskiworks.ghwatch.alarm.MarkNotifiationAsReadReceiver;
import com.daskiworks.ghwatch.backend.RemoteSystemClient.Response;
import com.daskiworks.ghwatch.image.ImageLoader;
import com.daskiworks.ghwatch.model.BaseViewData;
import com.daskiworks.ghwatch.model.LoadingStatus;
import com.daskiworks.ghwatch.model.Notification;
import com.daskiworks.ghwatch.model.NotificationStream;
import com.daskiworks.ghwatch.model.NotificationStreamViewData;
import com.daskiworks.ghwatch.model.StringViewData;

/**
 * Service used to work with unread notifications.
 * 
 * @author Vlastimil Elias <vlastimil.elias@worldonline.cz>
 */
public class UnreadNotificationsService {

  private static final String TAG = "UnreadNotificationsService";

  /**
   * URL to load notifications from.
   */
  private static final String URL_NOTIFICATIONS = GHConstants.URL_BASE + "/notifications";
  // private static final String URL_NOTIFICATIONS = GHConstants.URL_BASE+"/notifications?all=true";

  private static final String URL_REPOS = GHConstants.URL_BASE + "/repos/";
  private static final String URL_THREADS = GHConstants.URL_BASE + "/notifications/threads/";

  /**
   * Name of file where data are persisted.
   */
  private static final String persistFileName = "NotificationsUnread.td";

  /**
   * Reload from server is forced automatically if data in persistent store are older than this timeout [millis]
   */
  private static final long FORCE_VIEW_RELOAD_AFTER = 5 * Utils.MILLIS_MINUTE;

  /**
   * Id of android notification so we can update it.
   */
  public static final int ANDROID_NOTIFICATION_ID = 0;

  private Context context;

  private File persistFile;

  private AuthenticationManager authenticationManager;

  /**
   * Create service.
   * 
   * @param context this service runs in
   */
  public UnreadNotificationsService(Context context) {
    this.context = context;
    persistFile = context.getFileStreamPath(persistFileName);
    this.authenticationManager = AuthenticationManager.getInstance();
  }

  /**
   * Get unread notifications for view.
   * 
   * @param reloadStrategy if data should be reloaded from server
   * @return info about notifications
   */
  public NotificationStreamViewData getNotificationStreamForView(ViewDataReloadStrategy reloadStrategy) {

    NotificationStreamViewData nswd = new NotificationStreamViewData();
    NotificationStream ns = null;
    synchronized (TAG) {
      NotificationStream oldNs = Utils.readFromStore(TAG, context, persistFile);

      // user from store if possible, apply timeout of data from store
      if (reloadStrategy == ViewDataReloadStrategy.IF_TIMED_OUT) {
        ns = oldNs;
        if (ns != null && ns.getLastFullUpdateTimestamp() < (System.currentTimeMillis() - FORCE_VIEW_RELOAD_AFTER))
          ns = null;
      } else if (reloadStrategy == ViewDataReloadStrategy.NEVER) {
        ns = oldNs;
      }

      // read from server
      try {
        if (ns == null && reloadStrategy != ViewDataReloadStrategy.NEVER) {
          // we DO NOT use lastModified here because it returns only notifications newly added after given date, not all unread
          ns = readNotificationStreamFromServer(URL_NOTIFICATIONS, null);
          if (ns != null) {
            Utils.writeToStore(TAG, context, persistFile, ns);
            updateWidgets();
          }
        }
      } catch (InvalidObjectException e) {
        nswd.loadingStatus = LoadingStatus.DATA_ERROR;
        Log.w(TAG, "NotificationStream loading failed due data format problem: " + e.getMessage(), e);
      } catch (NoRouteToHostException e) {
        nswd.loadingStatus = LoadingStatus.CONN_UNAVAILABLE;
        Log.d(TAG, "NotificationStream loading failed due connection not available.");
      } catch (AuthenticationException e) {
        nswd.loadingStatus = LoadingStatus.AUTH_ERROR;
        Log.d(TAG, "NotificationStream loading failed due authentication problem: " + e.getMessage());
      } catch (IOException e) {
        nswd.loadingStatus = LoadingStatus.CONN_ERROR;
        Log.w(TAG, "NotificationStream loading failed due connection problem: " + e.getMessage());
      } catch (JSONException e) {
        nswd.loadingStatus = LoadingStatus.DATA_ERROR;
        Log.w(TAG, "NotificationStream loading failed due data format problem: " + e.getMessage());
      } catch (Exception e) {
        nswd.loadingStatus = LoadingStatus.UNKNOWN_ERROR;
        Log.e(TAG, "NotificationStream loading failed due: " + e.getMessage(), e);
      }

      // Show content from store because we are unable to read new one but want to show something
      if (ns == null)
        ns = oldNs;

      nswd.notificationStream = ns;
      return nswd;
    }
  }

  public BaseViewData markNotificationAsRead(long id) {
    BaseViewData nswd = new BaseViewData();
    try {
      RemoteSystemClient.postNoData(context, authenticationManager.getGhApiCredentials(context), URL_THREADS + id, null);
      synchronized (TAG) {
        NotificationStream oldNs = Utils.readFromStore(TAG, context, persistFile);
        if (oldNs != null) {
          oldNs.removeNotificationById(id);
          Utils.writeToStore(TAG, context, persistFile, oldNs);
          updateWidgets();
        }
      }
    } catch (NoRouteToHostException e) {
      nswd.loadingStatus = LoadingStatus.CONN_UNAVAILABLE;
    } catch (AuthenticationException e) {
      nswd.loadingStatus = LoadingStatus.AUTH_ERROR;
    } catch (IOException e) {
      Log.w(TAG, "NotificationRead marking failed due connection problem: " + e.getMessage());
      nswd.loadingStatus = LoadingStatus.CONN_ERROR;
    } catch (Exception e) {
      Log.e(TAG, "NotificationRead marking failed due: " + e.getMessage(), e);
      nswd.loadingStatus = LoadingStatus.UNKNOWN_ERROR;
    }
    return nswd;
  }

  public BaseViewData muteNotificationThread(long id) {
    BaseViewData nswd = new BaseViewData();
    try {
      RemoteSystemClient.putToURL(context, authenticationManager.getGhApiCredentials(context), URL_THREADS + id + "/subscription", null, "{\"ignored\":true}");
      // #49 mark it as read also to be removed from list
      markNotificationAsRead(id);
    } catch (NoRouteToHostException e) {
      nswd.loadingStatus = LoadingStatus.CONN_UNAVAILABLE;
    } catch (AuthenticationException e) {
      nswd.loadingStatus = LoadingStatus.AUTH_ERROR;
    } catch (IOException e) {
      Log.w(TAG, "Notification thread mute failed due connection problem: " + e.getMessage());
      nswd.loadingStatus = LoadingStatus.CONN_ERROR;
    } catch (Exception e) {
      Log.e(TAG, "Notification thread mute failed due: " + e.getMessage(), e);
      nswd.loadingStatus = LoadingStatus.UNKNOWN_ERROR;
    }
    return nswd;
  }

  public BaseViewData markAllNotificationsAsRead(String repository) {
    BaseViewData nswd = new BaseViewData();
    try {
      String url = URL_NOTIFICATIONS;
      if (repository != null) {
        url = URL_REPOS + repository + "/notifications";
      }
      RemoteSystemClient.putToURL(context, authenticationManager.getGhApiCredentials(context), url, null, "{}");
    } catch (NoRouteToHostException e) {
      nswd.loadingStatus = LoadingStatus.CONN_UNAVAILABLE;
    } catch (AuthenticationException e) {
      nswd.loadingStatus = LoadingStatus.AUTH_ERROR;
    } catch (IOException e) {
      Log.w(TAG, "NotificationRead marking failed due connection problem: " + e.getMessage());
      nswd.loadingStatus = LoadingStatus.CONN_ERROR;
    } catch (Exception e) {
      Log.e(TAG, "NotificationRead marking failed due: " + e.getMessage(), e);
      nswd.loadingStatus = LoadingStatus.UNKNOWN_ERROR;
    }
    return nswd;
  }

  /**
   * @param apiUrl API URL of Github object to get HTML url for.
   * @return response with URL from data
   */
  public StringViewData getGithubDataHtmlUrl(String apiUrl) {
    StringViewData nswd = new StringViewData();
    try {
      if (apiUrl != null) {
        Response<JSONObject> resp = RemoteSystemClient.getJSONObjectFromUrl(context, authenticationManager.getGhApiCredentials(context), apiUrl, null);
        nswd.data = Utils.trimToNull(resp.data.getString("html_url"));
        if (nswd.data == null) {
          Log.w(TAG, "GithubDataHtmlUrl loading failed due data format problem: no 'html_url' field in response");
          nswd.loadingStatus = LoadingStatus.DATA_ERROR;
        }
      }
    } catch (InvalidObjectException e) {
      nswd.loadingStatus = LoadingStatus.DATA_ERROR;
      Log.w(TAG, "NotificationStream loading failed due data format problem: " + e.getMessage(), e);
    } catch (NoRouteToHostException e) {
      nswd.loadingStatus = LoadingStatus.CONN_UNAVAILABLE;
    } catch (AuthenticationException e) {
      nswd.loadingStatus = LoadingStatus.AUTH_ERROR;
    } catch (IOException e) {
      Log.w(TAG, "GithubDataHtmlUrl loading failed due connection problem: " + e.getMessage());
      nswd.loadingStatus = LoadingStatus.CONN_ERROR;
    } catch (JSONException e) {
      nswd.loadingStatus = LoadingStatus.DATA_ERROR;
      Log.w(TAG, "GithubDataHtmlUrl loading failed due data format problem: " + e.getMessage());
    } catch (Exception e) {
      Log.e(TAG, "NotificationRead marking failed due: " + e.getMessage(), e);
      nswd.loadingStatus = LoadingStatus.UNKNOWN_ERROR;
    }
    return nswd;
  };

  /**
   * Switch if we will use optimized pooling or not.
   */
  private static boolean USE_OPTIMIZED_POOLING = true;

  private static long BACKGROUND_FORCE_FULL_RELOAD_AFTER = Utils.MILLIS_HOUR * 6L;
  private static long BACKGROUND_FORCE_FULL_RELOAD_AFTER_WIFI = Utils.MILLIS_HOUR * 1L;

  /**
   * Prepare "Last-Modified" content which is used to do optimized calls to GitHub rest API by incremental updates. It decides based on
   * {@link #USE_OPTIMIZED_POOLING} switch and on time of last full update. We do full updates once a time to prevent problems with incremental updates.
   * 
   * @param oldNs used to prepare header content
   * @return header content
   */
  public String prepareLastModifiedHeaderContent(NotificationStream oldNs, boolean isWiFi) {

    if (PreferencesUtils.getBoolean(context, PreferencesUtils.PREF_SERVER_CHECK_FULL, false))
      return null;

    long ts = 0;
    if (isWiFi) {
      ts = System.currentTimeMillis() - BACKGROUND_FORCE_FULL_RELOAD_AFTER_WIFI;
    } else {
      ts = System.currentTimeMillis() - BACKGROUND_FORCE_FULL_RELOAD_AFTER;
    }

    if (USE_OPTIMIZED_POOLING && oldNs != null && oldNs.getLastFullUpdateTimestamp() > ts) {
      return oldNs.getLastModified();
    }
    return null;
  }

  /**
   * Return true if background check of unread notifications is necessary.
   * 
   * @param context to be used
   * @return true if check is necessary
   */
  public static boolean isUnreadNotificationsServerCheckNecessary(Context context) {
    return PreferencesUtils.getBoolean(context, PreferencesUtils.PREF_NOTIFY, true)
        || PreferencesUtils.getBoolean(context, PreferencesUtils.PREF_WIDGET_UNREAD_EXISTS, false);
  }

  /**
   * Check new notifications on GitHub and fire androidNotification if necessary.
   * <p>
   * Check is done asynchronously, new thread is started inside of this method.
   * 
   * @see #newNotificationCheckImpl()
   */
  public void newNotificationCheck() {
    Thread t = new Thread() {
      public void run() {
        newNotificationCheckImpl();
      }
    };
    t.start();
  }

  /**
   * Real business logic for check new notifications on GitHub and fire androidNotification if necessary.
   * 
   * @see #newNotificationCheck()
   */
  protected void newNotificationCheckImpl() {
    Log.d(TAG, "Notification check started");
    try {
      synchronized (TAG) {
        NotificationStream oldNs = Utils.readFromStore(TAG, context, persistFile);

        String lastModified = prepareLastModifiedHeaderContent(oldNs, Utils.isInternetConnectionAvailableWifi(Utils.getConnectivityManager(context)));

        NotificationStream ns = readNotificationStreamFromServer(URL_NOTIFICATIONS, lastModified);

        if (ns != null) {
          if (lastModified != null) {
            // incremental update has been performed and some new notif is available (ns is not null), so we have to add old ones to keep them
            for (Notification n : oldNs)
              ns.addNotification(n);
            ns.setLastFullUpdateTimestamp(oldNs.getLastFullUpdateTimestamp());
          }
          Utils.writeToStore(TAG, context, persistFile, ns);

          fireAndroidNotification(ns, oldNs);
          updateWidgetsFromBackgroundCheck(ns, oldNs);
        }
      }
    } catch (NoRouteToHostException e) {
      Log.d(TAG, "Notification check failed due: " + e.getMessage());
    } catch (Exception e) {
      Log.w(TAG, "Notification check failed due: " + e.getMessage());
    } finally {
      PreferencesUtils.storeLong(context, PreferencesUtils.INT_SERVERINFO_LASTUNREADNOTIFBACKREQUESTTIMESTAMP, System.currentTimeMillis());
      Log.d(TAG, "Notification check finished");
    }
  }

  /**
   * @param url
   * @param lastModified
   * @return null if lastModified used and nothing new
   * @throws InvalidObjectException
   * @throws NoRouteToHostException
   * @throws AuthenticationException
   * @throws IOException
   * @throws JSONException
   * @throws URISyntaxException
   */
  protected NotificationStream readNotificationStreamFromServer(String url, String lastModified) throws InvalidObjectException, NoRouteToHostException,
      AuthenticationException, IOException, JSONException, URISyntaxException {

    Map<String, String> headers = null;
    if (lastModified != null) {
      headers = new HashMap<String, String>();
      headers.put("If-Modified-Since", lastModified);
    }

    Response<JSONArray> resp = RemoteSystemClient.getJSONArrayFromUrl(context, authenticationManager.getGhApiCredentials(context), url, headers);

    if (resp.notModified)
      return null;

    NotificationStream ns = NotificationStreamParser.parseNotificationStream(resp.data);
    ns.setLastModified(resp.lastModified);
    if (lastModified == null)
      ns.setLastFullUpdateTimestamp(System.currentTimeMillis());
    return ns;
  }

  /**
   * Call this when you want to mark android notification as read - remove it
   */
  public void markAndroidNotificationsRead() {
    Utils.getNotificationManager(context).cancel(ANDROID_NOTIFICATION_ID);
  }

  /**
   * Call this when you want to reset notification alert in widgets (cancel highlight)
   */
  public void markAndroidWidgetsAsRead() {
    if (PreferencesUtils.getBoolean(context, PreferencesUtils.PREF_WIDGET_UNREAD_EXISTS, false)) {
      PreferencesUtils.storeBoolean(context, PreferencesUtils.PREF_WIDGET_UNREAD_HIGHLIGHT, false);
      updateWidgets();
    }
  }

  protected NotificationStream filterForAndroidNotification(NotificationStream ns) {
    NotificationStream ret = new NotificationStream();

    for (Notification n : ns) {
      String p = PreferencesUtils.getNotificationFilterForRepository(context, n.getRepositoryFullName(), true);
      if (!PreferencesUtils.PREF_NOTIFY_FILTER_NOTHING.equalsIgnoreCase(p)) {
        if (PreferencesUtils.PREF_NOTIFY_FILTER_ALL.equalsIgnoreCase(p) || !"subscribed".equalsIgnoreCase(n.getReason())) {
          ret.addNotification(n);
        }
      }
    }

    return ret;
  }

  protected void fireAndroidNotification(NotificationStream newStream, NotificationStream oldStream) {
    if (newStream == null || !PreferencesUtils.getBoolean(context, PreferencesUtils.PREF_NOTIFY, true))
      return;

    Log.d(TAG, "fireAndroidNotification count before filter " + newStream.size());
    newStream = filterForAndroidNotification(newStream);
    Log.d(TAG, "fireAndroidNotification count after filter " + newStream.size());
    if (newStream.isNewNotification(oldStream)) {

      NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context).setSmallIcon(R.drawable.github_notification)
          .setContentTitle(context.getString(R.string.an_title_more)).setPriority(NotificationCompat.PRIORITY_DEFAULT);
      mBuilder.setAutoCancel(true);

      if (newStream.size() > 1)
        mBuilder.setNumber(newStream.size());

      boolean allFromOne = newStream.allNotificationsFromSameRepository();

      if (newStream.size() == 1 || allFromOne) {
        // only one repository
        Notification n = newStream.get(0);
        Bitmap b = ImageLoader.getInstance(context).loadImageWithFileLevelCache(n.getRepositoryAvatarUrl());
        if (b != null) {
          mBuilder.setLargeIcon(b);
        } else {
          mBuilder.setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.github_notification));
        }
        mBuilder.setContentText(n.getRepositoryFullName());
      } else {
        mBuilder.setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.drawable.github_notification));
      }

      Intent resultIntent = null;
      if (newStream.size() == 1) {
        mBuilder.setContentTitle(context.getString(R.string.an_title_one));
        Notification n = newStream.get(0);
        mBuilder.setContentText(n.getRepositoryFullName() + ": " + n.getSubjectTitle());
        NotificationCompat.BigTextStyle btStyle = new NotificationCompat.BigTextStyle();
        btStyle.bigText(n.getSubjectTitle());
        btStyle.setSummaryText(n.getRepositoryFullName());
        mBuilder.setStyle(btStyle);
        Intent actionIntent = new Intent(context, MarkNotifiationAsReadReceiver.class);
        actionIntent.putExtra(MarkNotifiationAsReadReceiver.INTENT_EXTRA_KEY_ID, n.getId());
        mBuilder.addAction(R.drawable.ic_action_dismis_all, context.getString(R.string.action_mark_read),
            PendingIntent.getBroadcast(context, 0, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT));

        resultIntent = new Intent(context, MainActivity.class);
      } else {
        NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
        for (Notification n : newStream) {
          if (allFromOne) {
            inboxStyle.addLine(n.getSubjectTitle());
          } else {
            inboxStyle.addLine(n.getRepositoryFullName() + ": " + n.getSubjectTitle());
          }
        }
        if (allFromOne)
          inboxStyle.setSummaryText(newStream.get(0).getRepositoryFullName());
        else
          inboxStyle.setSummaryText(" ");
        mBuilder.setStyle(inboxStyle);

        Intent actionIntent = new Intent(context, MainActivity.class);
        actionIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
        actionIntent.setAction(MainActivity.INTENT_ACTION_DISMISS_ALL);
        mBuilder
            .addAction(R.drawable.ic_action_dismis_all, context.getString(R.string.action_all_read), PendingIntent.getActivity(context, 0, actionIntent, 0));

        resultIntent = new Intent(context, MainActivity.class);
      }

      resultIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
      resultIntent.setAction(MainActivity.INTENT_ACTION_SHOW);
      PendingIntent resultPendingIntent = PendingIntent.getActivity(context, 0, resultIntent, 0);
      mBuilder.setContentIntent(resultPendingIntent);

      String nsound = PreferencesUtils.getString(context, PreferencesUtils.PREF_NOTIFY_SOUND, null);
      Log.d(TAG, "Notification sound from preference: " + nsound);
      if (nsound != null) {
        mBuilder.setSound(Uri.parse(nsound));
      }
      if (PreferencesUtils.getBoolean(context, PreferencesUtils.PREF_NOTIFY_VIBRATE, true)) {
        mBuilder.setVibrate(new long[] { 0, 300, 100, 150, 100, 150 });
      }

      mBuilder.setLights(0xffffffff, 100, 4000);

      // mId allows you to update the notification later on.
      Utils.getNotificationManager(context).notify(ANDROID_NOTIFICATION_ID, mBuilder.build());
      ActivityTracker.sendEvent(context, ActivityTracker.CAT_NOTIF, "new_notif", "notif count: " + newStream.size(), Long.valueOf(newStream.size()));
    } else if (newStream.isEmpty()) {
      // #54 dismiss previous android notification if no any Github notification is available (as it was read on another device)
      Utils.getNotificationManager(context).cancel(ANDROID_NOTIFICATION_ID);
    }
  }

  protected void updateWidgetsFromBackgroundCheck(NotificationStream newStream, NotificationStream oldStream) {
    if (newStream == null)
      return;
    if (PreferencesUtils.getBoolean(context, PreferencesUtils.PREF_WIDGET_UNREAD_EXISTS, false)) {
      if (newStream.isNewNotification(oldStream)) {
        PreferencesUtils.storeBoolean(context, PreferencesUtils.PREF_WIDGET_UNREAD_HIGHLIGHT, true);
      }
      updateWidgets();
    }
  }

  protected void updateWidgets() {
    if (PreferencesUtils.getBoolean(context, PreferencesUtils.PREF_WIDGET_UNREAD_EXISTS, false)) {
      AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
      int[] ids = appWidgetManager.getAppWidgetIds(new ComponentName(context, UnreadAppWidgetProvider.class));
      Intent intent = new Intent(context, UnreadAppWidgetProvider.class);
      intent.setAction("android.appwidget.action.APPWIDGET_UPDATE");
      intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
      context.sendBroadcast(intent);
      Log.d(TAG, "Widget update Intent fired");
    }
  }

  public void flushPersistentStore() {
    persistFile.delete();
  }

}




Java Source Code List

com.daskiworks.ghwatch.AboutDialogFragment.java
com.daskiworks.ghwatch.ActivityBase.java
com.daskiworks.ghwatch.ActivityTracker.java
com.daskiworks.ghwatch.FullHeightListView.java
com.daskiworks.ghwatch.IabHelper.java
com.daskiworks.ghwatch.LoginDialogFragment.java
com.daskiworks.ghwatch.MainActivity.java
com.daskiworks.ghwatch.MyBackupAgent.java
com.daskiworks.ghwatch.NotificationListAdapter.java
com.daskiworks.ghwatch.NotificationRepositoriesListAdapter.java
com.daskiworks.ghwatch.SettingsActivity.java
com.daskiworks.ghwatch.StartActivity.java
com.daskiworks.ghwatch.SupportAppDevelopmentDialogFragment.java
com.daskiworks.ghwatch.SwipeDismissListViewTouchListener.java
com.daskiworks.ghwatch.UnreadAppWidgetProvider.java
com.daskiworks.ghwatch.Utils.java
com.daskiworks.ghwatch.WatchedRepositoriesActivity.java
com.daskiworks.ghwatch.WatchedRepositoryListAdapter.java
com.daskiworks.ghwatch.alarm.AlarmBroadcastReceiver.java
com.daskiworks.ghwatch.alarm.MarkNotifiationAsReadReceiver.java
com.daskiworks.ghwatch.backend.AuthenticationManager.java
com.daskiworks.ghwatch.backend.GHConstants.java
com.daskiworks.ghwatch.backend.NotificationStreamParser.java
com.daskiworks.ghwatch.backend.OTPAuthenticationException.java
com.daskiworks.ghwatch.backend.PreferencesUtils.java
com.daskiworks.ghwatch.backend.RemoteSystemClient.java
com.daskiworks.ghwatch.backend.UnreadNotificationsService.java
com.daskiworks.ghwatch.backend.ViewDataReloadStrategy.java
com.daskiworks.ghwatch.backend.WatchedRepositoriesParser.java
com.daskiworks.ghwatch.backend.WatchedRepositoriesService.java
com.daskiworks.ghwatch.image.FileCache.java
com.daskiworks.ghwatch.image.ImageLoader.java
com.daskiworks.ghwatch.image.MemoryCache.java
com.daskiworks.ghwatch.model.AccountType.java
com.daskiworks.ghwatch.model.BaseViewData.java
com.daskiworks.ghwatch.model.BooleanViewData.java
com.daskiworks.ghwatch.model.GHCredentials.java
com.daskiworks.ghwatch.model.GHUserInfo.java
com.daskiworks.ghwatch.model.GHUserLoginInfo.java
com.daskiworks.ghwatch.model.LoadingStatus.java
com.daskiworks.ghwatch.model.NotifCount.java
com.daskiworks.ghwatch.model.NotificationStreamViewData.java
com.daskiworks.ghwatch.model.NotificationStream.java
com.daskiworks.ghwatch.model.Notification.java
com.daskiworks.ghwatch.model.Repository.java
com.daskiworks.ghwatch.model.StringViewData.java
com.daskiworks.ghwatch.model.WatchedRepositoriesViewData.java
com.daskiworks.ghwatch.model.WatchedRepositories.java
com.daskiworks.ghwatch.view.preference.ShowTextPreference.java
com.daskiworks.ghwatch.view.preference.ShowTimestampPreference.java