Android Open Source - feedhive Scheduled Update Service






From Project

Back to project page feedhive.

License

The source code is released under:

SOFTWARE LICENSE ---------------- Copyright (C) 2012, 2013, 2014 Younghyung Cho. <yhcting77@gmail.com> All rights reserved. This file is part of FeedHive This program is licensed under the FreeBSD l...

If you think the Android project feedhive 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 (C) 2012, 2013, 2014/* w ww .  ja  v  a 2  s.  co m*/
 * Younghyung Cho. <yhcting77@gmail.com>
 * All rights reserved.
 *
 * This file is part of FeedHive
 *
 * This program is licensed under the FreeBSD license
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation
 * are those of the authors and should not be interpreted as representing
 * official policies, either expressed or implied, of the FreeBSD Project.
 *****************************************************************************/

package free.yhc.feeder;

import static free.yhc.feeder.model.Utils.eAssert;

import java.util.Calendar;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.IBinder;
import free.yhc.feeder.db.ColumnChannel;
import free.yhc.feeder.db.DBPolicy;
import free.yhc.feeder.model.BGTaskUpdateChannel;
import free.yhc.feeder.model.BaseBGTask;
import free.yhc.feeder.model.Environ;
import free.yhc.feeder.model.Err;
import free.yhc.feeder.model.RTTask;
import free.yhc.feeder.model.UnexpectedExceptionHandler;
import free.yhc.feeder.model.Utils;

// There is no way to notify result of scheduled-update to user.
// So, even if scheduled update may fail, there is no explicit notification.
// But user can know whether scheduled-update successes or fails by checking age since last successful update.
// (See channelListAdapter for 'age' time)
public class ScheduledUpdateService extends Service implements
UnexpectedExceptionHandler.TrackedModule {
    private static final boolean DBG = false;
    private static final Utils.Logger P = new Utils.Logger(ScheduledUpdateService.class);

    // Should match manifest's intent filter
    private static final String SCHEDUPDATE_INTENT_ACTION = "feeder.intent.action.SCHEDULED_UPDATE";

    private static final String CMD_ALARM   = "alarm";
    private static final String CMD_RESCHED = "resched";
    private static final String CMD_UPDATE  = "update";

    private static final String KEY_CMD     = "cmd";
    private static final String KEY_TIME    = "time";
    private static final String KEY_CHANS   = "chans";

    private static final int    RETRY_DELAY = 1000; // ms

    // Number of running service command
    // This is used to check whether there is running scheduled update service instance or not.
    // NOTE
    // This variable only be accessed by main UI thread!
    private static int                   mSrvcnt = 0;

    // Scheduled update is enabled/disabled.
    // If disabled, requested command is continuously posted to message Q until
    //   scheduled update is re-enabled again.
    private static boolean               mEnabled = true;

    private final DBPolicy               mDbp = DBPolicy.get();
    private final RTTask                 mRtt = RTTask.get();
    private final HashSet<Long>          mTaskset = new HashSet<Long>();

    // If time is changed Feeder need to re-scheduling scheduled-update.
    public static class DateChangedReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            // TODO
            // At some devices, time is set whenever device back to active from sleep.
            // So, this receiver is called too often.
            // To avoid this, just ignore this intent until any better solution is found.
            // => Back to active this code again. Rescheduling often is no harmful and
            //      sometimes it keep App. safe from unexpected-not-scheduled-bug.
            //
            // FIXME
            // Because Feeder ignores this intent, when system time is changed,
            //   scheduled update isn't re-scheduled!
            // This is bug, but in most cases, system time is not changed manually.
            // So, let's ignore this exceptional case.
            //
            // TODO
            // Any better way??
            Intent svc = new Intent(context, ScheduledUpdateService.class);
            svc.putExtra(KEY_CMD, CMD_RESCHED);
            // onStartCommand will be sent!
            context.startService(svc);
            // Update should be started before falling into sleep.
            LifeSupportService.getWakeLock();
        }
    }

    // Why broadcast receiver?
    // Using pendingIntent from "PendingIntent.getService()" doesn't guarantee that
    //   given service gets controls before device falls into sleep.
    // But broadcast receiver guarantee that device doesn't fall into sleep before returning from 'onReceive'
    // (This is described at Android document)
    // So, broadcast receiver is used event if it's more complex than using "PendingIntent.getService()" directly.
    public static class AlarmReceiver extends BroadcastReceiver {
        // NOTE:
        //   broadcast receiver is run on main ui thread (same as service).
        @Override
        public void
        onReceive(Context context, Intent intent) {
            //logI("AlarmReceiver : onReceive");
            long time = intent.getLongExtra("time", -1);
            if (time < 0) {
                eAssert(false);
                return;
            }
            Intent svc = new Intent(context, ScheduledUpdateService.class);
            svc.putExtra(KEY_CMD, CMD_ALARM);
            svc.putExtra(KEY_TIME, time);
            // onStartCommand will be sent!
            context.startService(svc);
            // Update should be started before falling into sleep.
            LifeSupportService.getWakeLock();
        }
    }

    private class UpdateBGTask extends BGTaskUpdateChannel {
        private final EventListener   mListener = new EventListener();
        private long            mCid = -1;
        private int             mStartId = -1;

        private class EventListener extends BaseBGTask.OnEventListener {
            @Override
            public void
            onCancelled(BaseBGTask task, Object param) {
                //logI("ScheduledUpdateService(onCancel) : " + cid);
                synchronized (mTaskset) {
                    mTaskset.remove(mCid);
                    //logI("    mTaskset size : " + mTaskset.size());
                    if (mTaskset.isEmpty())
                        stopSelf();
                }
            }

            @Override
            public void
            onPostRun(BaseBGTask task, Err result) {
                //logI("ScheduledUpdateService(onPostRun) : " + cid + " (" + getResources().getText(result.getMsgId()) + ")");
                synchronized (mTaskset) {
                    mTaskset.remove(mCid);
                    if (mTaskset.isEmpty())
                        stopSelf();
                }
            }
        }

        UpdateBGTask(long cid, int startId, BGTaskUpdateChannel.Arg arg) {
            super(arg);
            mCid = cid;
            mStartId = startId;
        }

        BaseBGTask.OnEventListener
        getTaskEventListener() {
            return mListener;
        }
    }

    private class StartCmdPost implements Runnable {
        private Intent _mIntent;
        private int    _mFlags;
        private int    _mStartId;
        StartCmdPost(Intent intent, int flags, int startId) {
            _mIntent = intent;
            _mFlags = flags;
            _mStartId = startId;
        }

        @Override
        public void run() {
            // try after 500 ms with same runnable instance.
            if (!ScheduledUpdateService.isEnabled()) {
                //logI("runStartCommand --- POST!!!");
                Environ.getUiHandler().postDelayed(this, RETRY_DELAY);
            } else
                runStartCommand(_mIntent, _mFlags, _mStartId);
        }
    }

    private static class ChannelValue {
        long cid;
        long v;
        ChannelValue(long cid, long v) {
            this.cid = cid;
            this.v = v;
        }
    }

    private static class ChannelValueComparator implements Comparator<ChannelValue> {
        @Override
        public int compare(ChannelValue cv0, ChannelValue cv1) {
            if (cv0.v < cv1.v)
                return -1;
            else if (cv0.v > cv1.v)
                return 1;
            else
                return 0;
        }
    }

    // =======================================================
    //
    // Function to handle race-condition regarding DB access!
    //
    // =======================================================

    /**
     * Should be called on main UI thread.
     */
    private static void
    incInstanceCount() {
        eAssert(Utils.isUiThread() && mSrvcnt >= 0);
        mSrvcnt++;
    }

    /**
     * Should be called on main UI thread.
     */
    private static void
    decInstanceCount() {
        eAssert(Utils.isUiThread() && mSrvcnt > 0);
        mSrvcnt--;
    }

    /**
     * Should be called on main UI thread.
     */
    public static boolean
    doesInstanceExist() {
        eAssert(Utils.isUiThread());
        return mSrvcnt > 0;
    }

    /**
     * This is very dangerous! (May lead to infinite loop!)
     * So, DO NOT USER this function if you don't know what your are doing!
     * Should be called on main UI thread.
     */
    public static void
    enable() {
        eAssert(Utils.isUiThread());
        mEnabled = true;
    }

    /**
     * This is very dangerous! (May lead to infinite loop!)
     * So, DO NOT USER this function if you don't know what your are doing!
     * Should be called on main UI thread.
     */
    public static void
    disable() {
        eAssert(Utils.isUiThread());
        mEnabled = false;
    }

    /**
     * Should be called on main UI thread.
     */
    private static boolean
    isEnabled() {
        eAssert(Utils.isUiThread());
        return mEnabled;
    }

    // =======================================================
    //
    //
    //
    // =======================================================

    // out[0] : time to go from dayTime0 to dayTime1
    // out[1] : time to go from dayTime1 to dayTime0
    private static void
    dayBasedDistanceMs(long[] out, long dayms0, long dayms1) {
        eAssert(dayms0 <= Utils.DAY_IN_MS && dayms1 <= Utils.DAY_IN_MS);
        if (dayms0 > dayms1) {
            out[1] = dayms0 - dayms1;
            out[0] = Utils.DAY_IN_MS - out[1];
        } else if (dayms0 < dayms1) {
            out[0] = dayms1 - dayms0;
            out[1] = Utils.DAY_IN_MS - out[0];
        }
    }

    /**
     * Schedule update for 'cids' right now.
     * @param cids
     */
    static void
    scheduleImmediateUpdate(long[] cids) {
        Context context = Environ.getAppContext();
        Intent svc = new Intent(context, ScheduledUpdateService.class);
        svc.putExtra(KEY_CMD, CMD_UPDATE);
        svc.putExtra(KEY_CHANS, cids);
        // onStartCommand will be sent!
        context.startService(svc);
        // Update should be started before falling into sleep.
        LifeSupportService.getWakeLock();
    }

    /**
     * NOTE
     * Next updates which are at least 1-min after, will be scheduled.
     * @param calNow
     */
    static void
    scheduleNextUpdate(Calendar calNow) {
        long daybase = Utils.dayBaseMs(calNow);
        long dayms = calNow.getTimeInMillis() - daybase;
        if (dayms < 0)
            dayms = 0; // To compensate 1 sec error from '/' operation.
        eAssert(dayms <= Utils.DAY_IN_MS);

        // If we get killed, after returning from here, restart
        Cursor c = DBPolicy.get().queryChannel(ColumnChannel.SCHEDUPDATETIME);
        if (!c.moveToFirst()) {
            c.close();
            return; // There is no channel.
        }

        final long invalidNearestNext = Utils.DAY_IN_MS * 2;
        long nearestNext = invalidNearestNext; // large enough value.
        do {
            String sStr = c.getString(0);
            if (Utils.isValidValue(sStr)) {
                // NOTE : IMPORTANT
                //   Time stored at DB is HOUR_OF_DAY (0 - 23)
                //   (See comments regarding Column at DB.)
                //   We cannot guarantee that service is started at exact time.
                //   So, we should compensate it (see comments at 'onStartCommand'
                long[] secs   = Utils.nStringToNrs(sStr);
                long[] out  = new long[2];
                for (long s : secs) {
                    long sms = Utils.secToMs(s);
                    dayBasedDistanceMs(out, dayms, sms);

                    // out[0] is time to go from 'dayms' to 'hms'
                    if (out[0] < nearestNext)
                        nearestNext = out[0];
                }
            }
        } while (c.moveToNext());
        c.close();

        if (nearestNext != invalidNearestNext) {
            Context cxt = Environ.getAppContext();
            // convert into real time.
            nearestNext += calNow.getTimeInMillis();
            Intent intent = new Intent(cxt, AlarmReceiver.class);
            intent.setAction(SCHEDUPDATE_INTENT_ACTION);
            intent.putExtra("time", nearestNext);
            intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
            PendingIntent pIntent = PendingIntent.getBroadcast(cxt,
                                                               0,
                                                               intent,
                                                               PendingIntent.FLAG_CANCEL_CURRENT);
            // Get the AlarmManager service
            AlarmManager am = (AlarmManager)cxt.getSystemService(ALARM_SERVICE);
            am.set(AlarmManager.RTC_WAKEUP, nearestNext, pIntent);
            if (DBG) P.v("New nearest scheduled update is set! " + (nearestNext / 1000) + " sec.");
        }
    }

    private void
    startUpdates(int startId, long[] cids) {
        for (long cid : cids) {
            // NOTE
            // onStartCommand() is run on UIThread.
            // So, I don't need to worry about race-condition caused from re-entrance of this function
            // That's why 'getState' and 'unregister/register' are not synchronized explicitly.
            RTTask.TaskState state = mRtt.getState(cid, RTTask.Action.UPDATE);
            if (RTTask.TaskState.CANCELING == state
                || RTTask.TaskState.RUNNING == state
                || RTTask.TaskState.READY == state) {
                //logI("doCmdAlarm : Channel [" + cid + "] is already active.\n" +
                //     "             So scheduled update is skipped");
            } else {
                // unregister gracefully to start update.
                // There is sanity check in BGTaskManager - see BGTaskManager
                //   to know why this 'unregister' is required.
                mRtt.unregister(cid, RTTask.Action.UPDATE);

                UpdateBGTask task = new UpdateBGTask(cid, startId, new BGTaskUpdateChannel.Arg(cid));
                mRtt.register(cid, RTTask.Action.UPDATE, task);
                mRtt.bind(cid, RTTask.Action.UPDATE, this, task.getTaskEventListener());
                //logI("doCmdAlarm : start update BGTask for [" + cid + "]");
                synchronized (mTaskset) {
                    if (!mTaskset.add(cid)) {
                        if (DBG) P.w("doCmdAlarm : starts duplicated update! : " + cid);
                    }
                }
                mRtt.start(cid, RTTask.Action.UPDATE);
            }
        }
    }

    private void
    doCmdResched(int startId) {
        // Just reschedule and return.
        scheduleNextUpdate(Calendar.getInstance());
    }

    private void
    doCmdAlarm(int startId, long schedTime) {
        if (DBG) P.v("DO scheduled update!! : " + startId);
        Calendar calNow = Calendar.getInstance();
        long daybase = Utils.dayBaseMs(calNow);
        long dayms = calNow.getTimeInMillis() - daybase;

        if (schedTime < 0 // something wrong!!
            || calNow.getTimeInMillis() < schedTime // scheduled too early
            || calNow.getTimeInMillis() > schedTime + Utils.HOUR_IN_MS) { // scheduled too late
            if (DBG) P.w("WARN : weired scheduling!!!\n" +
                         "    scheduled time(ms) : " + schedTime + "\n" +
                         "    current time(ms)   : " + calNow.getTimeInMillis());
            scheduleNextUpdate(calNow);
            return;
        }

        // If we get killed, after returning from here, restart
        Cursor c = mDbp.queryChannel(new ColumnChannel[] {
                ColumnChannel.ID,
                ColumnChannel.SCHEDUPDATETIME });
        // below values are 'Column Index' for above query.
        final int iId   = 0;
        final int iTime = 1;

        if (!c.moveToFirst()) {
            c.close();
            return; // There is no channel.
        }

        // NOTE : IMPORTANT
        // Service is started behind it's original plan by amount of 'schedError'
        // So, service should run scheduled-update whose planed-sched-time is
        //   between 'schedTime' and 'current'.
        long schedError = calNow.getTimeInMillis() - schedTime;
        LinkedList<Long> chl = new LinkedList<Long>();
        do {
            String sStr = c.getString(iTime);
            //   We cannot guarantee that service is started at exact time.
            //   So, we need to check error and find next scheduled based on this error.
            long[] ss  = Utils.nStringToNrs(sStr);
            long   cid = c.getLong(iId);
            long[] out = new long[2];
            for (long s : ss) {
                long sms = Utils.secToMs(s);
                dayBasedDistanceMs(out, dayms, sms);

                // out[1] is time to go from 'hms' to 'dayms'
                // This means 'time passed since 'hms' because 'dayms' is current time.
                if (out[1] <= schedError)
                    chl.add(cid);
            }
        } while (c.moveToNext());
        c.close();

        long[] cids = Utils.convertArrayLongTolong(chl.toArray(new Long[0]));
        startUpdates(startId, cids);

        // register next scheduled-update.
        // NOTE
        //   Real-Now - Calendar.getInstance() - SHOULD NOT be used here!
        //   We already have error between 'calNow' and Real-Now
        //     (we already spends some time to run some code!)
        //   But, some channels may be scheduled between these two time - 'calNow' and Real-Now.
        //   If Real-Now is chosen as current calendar, tasks mentioned at above line, is missed from scheduled-update!
        //   So, 'calNow' should be used as current calendar!
        scheduleNextUpdate(calNow);
    }

    private void
    doCmdUpdate(int startId, long[] cids) {
        if (DBG) {
            StringBuilder bldr = new StringBuilder("");
            bldr.append("DO Command update!! : " + startId + "\n");
            bldr.append("Cids: ");
            for (long cid : cids)
                bldr.append(cid + ", ");
            bldr.append("\n");
            P.v(bldr.toString());
        }

        startUpdates(startId, cids);
    }

    private void
    runStartCommand(Intent intent, int flags, int startId) {
        String cmd = intent.getStringExtra(KEY_CMD);
        //logI("ScheduledUpdate : runStartCommand : " + cmd);
        // 'cmd' can be null.
        // So, DO NOT use "cmd.equals()"...
        if (CMD_RESCHED.equals(cmd))
            doCmdResched(startId);
        else if (CMD_ALARM.equals(cmd)) {
            long schedTime = intent.getLongExtra(KEY_TIME, -1);
            doCmdAlarm(startId, schedTime);
        } else if (CMD_UPDATE.equals(cmd)) {
            long[] cids = intent.getLongArrayExtra(KEY_CHANS);
            doCmdUpdate(startId, cids);
        } else
            eAssert(false);

        synchronized (mTaskset) {
            if (mTaskset.isEmpty())
                stopSelf();
        }
    }

    @Override
    public String
    dump(UnexpectedExceptionHandler.DumpLevel lv) {
        return "[ ScheduledUpdateService ]";
    }

    @Override
    public void
    onCreate() {
        super.onCreate();
        UnexpectedExceptionHandler.get().registerModule(this);

        incInstanceCount();
    }

    // NOTE:
    //   Starting service requires getting WakeLock!
    //   onStartCommand is run on main ui thread (same as onReceive).
    //   So, we don't need to concern about race-condition between these two.
    @Override
    public int
    onStartCommand(Intent intent, int flags, int startId) {
        try {
            // try after some time again.
            if (!ScheduledUpdateService.isEnabled())
                Environ.getUiHandler().postDelayed(new StartCmdPost(intent, flags, startId), RETRY_DELAY);
            else
                runStartCommand(intent, flags, startId);
        } finally {
            // At any case wakelock should be released.
            // BGTask itself will manage wakelock for the background tasking.
            // We don't need to worry about update jobs.
            // Just release wakelock for this command.
            LifeSupportService.putWakeLock();
        }
        return START_NOT_STICKY;
    }

    @Override
    public IBinder
    onBind(Intent intent) {
        // We don't provide binding, so return null
        return null;
    }

    @Override
    public void
    onDestroy() {
        decInstanceCount();
        UnexpectedExceptionHandler.get().unregisterModule(this);
        //logI("ScheduledUpdateService : onDestroy");
        super.onDestroy();
    }
}




Java Source Code List

free.yhc.feeder.AppWidgetCategoryChooserActivity.java
free.yhc.feeder.AppWidgetMenuActivity.java
free.yhc.feeder.AppWidgetUpdateCategoryActivity.java
free.yhc.feeder.AsyncAdapter.java
free.yhc.feeder.AsyncCursorAdapter.java
free.yhc.feeder.AsyncCursorListAdapter.java
free.yhc.feeder.ChannelListActivity.java
free.yhc.feeder.ChannelListAdapter.java
free.yhc.feeder.ChannelListFragment.java
free.yhc.feeder.ChannelListPagerAdapter.java
free.yhc.feeder.ChannelSettingActivity.java
free.yhc.feeder.DBManagerActivity.java
free.yhc.feeder.DiagAsyncTask.java
free.yhc.feeder.FeederActivity.java
free.yhc.feeder.FeederApp.java
free.yhc.feeder.FeederPreferenceActivity.java
free.yhc.feeder.FragmentPagerAdapterEx.java
free.yhc.feeder.ItemListActivity.java
free.yhc.feeder.ItemListAdapter.java
free.yhc.feeder.ItemViewActivity.java
free.yhc.feeder.LifeSupportService.java
free.yhc.feeder.NotiManager.java
free.yhc.feeder.PredefinedChannelActivity.java
free.yhc.feeder.ScheduledUpdateService.java
free.yhc.feeder.UiHelper.java
free.yhc.feeder.appwidget.AppWidgetUtils.java
free.yhc.feeder.appwidget.Provider.java
free.yhc.feeder.appwidget.UpdateService.java
free.yhc.feeder.appwidget.ViewsFactory.java
free.yhc.feeder.appwidget.ViewsService.java
free.yhc.feeder.db.ColumnCategory.java
free.yhc.feeder.db.ColumnChannel.java
free.yhc.feeder.db.ColumnItem.java
free.yhc.feeder.db.DBPolicy.java
free.yhc.feeder.db.DB.java
free.yhc.feeder.model.AssetSQLiteHelper.java
free.yhc.feeder.model.AtomParser.java
free.yhc.feeder.model.BGTaskDownloadToFile.java
free.yhc.feeder.model.BGTaskDownloadToItemContent.java
free.yhc.feeder.model.BGTaskManager.java
free.yhc.feeder.model.BGTaskUpdateChannel.java
free.yhc.feeder.model.BGTask.java
free.yhc.feeder.model.BaseBGTask.java
free.yhc.feeder.model.ContentsManager.java
free.yhc.feeder.model.DelayedAction.java
free.yhc.feeder.model.Environ.java
free.yhc.feeder.model.Err.java
free.yhc.feeder.model.FeedParser.java
free.yhc.feeder.model.FeedPolicy.java
free.yhc.feeder.model.Feed.java
free.yhc.feeder.model.FeederException.java
free.yhc.feeder.model.HtmlParser.java
free.yhc.feeder.model.ItemActionHandler.java
free.yhc.feeder.model.KeyBasedLinkedList.java
free.yhc.feeder.model.ListenerManager.java
free.yhc.feeder.model.NetLoader.java
free.yhc.feeder.model.RSSParser.java
free.yhc.feeder.model.RTTask.java
free.yhc.feeder.model.ThreadEx.java
free.yhc.feeder.model.UnexpectedExceptionHandler.java
free.yhc.feeder.model.UsageReport.java
free.yhc.feeder.model.Utils.java