Android Open Source - feedhive Views Factory






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//from w  w w  . j  ava  2 s .c o  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.appwidget;

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

import java.io.File;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicReference;

import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.database.Cursor;
import android.view.View;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
import free.yhc.feeder.R;
import free.yhc.feeder.UiHelper;
import free.yhc.feeder.db.ColumnChannel;
import free.yhc.feeder.db.ColumnItem;
import free.yhc.feeder.db.DB;
import free.yhc.feeder.db.DBPolicy;
import free.yhc.feeder.model.BGTask;
import free.yhc.feeder.model.BaseBGTask;
import free.yhc.feeder.model.ContentsManager;
import free.yhc.feeder.model.Environ;
import free.yhc.feeder.model.Err;
import free.yhc.feeder.model.ItemActionHandler;
import free.yhc.feeder.model.ListenerManager;
import free.yhc.feeder.model.RTTask;
import free.yhc.feeder.model.UnexpectedExceptionHandler;
import free.yhc.feeder.model.Utils;

public class ViewsFactory implements
RemoteViewsService.RemoteViewsFactory,
UnexpectedExceptionHandler.TrackedModule {
    private static final boolean DBG = false;
    private static final Utils.Logger P = new Utils.Logger(ViewsFactory.class);

    private static final int    COLI_ID                 = 0;
    private static final int    COLI_CHANNELID          = 1;
    private static final int    COLI_TITLE              = 2;
    private static final int    COLI_DESCRIPTION        = 3;
    private static final int    COLI_ENCLOSURE_LENGTH   = 4;
    private static final int    COLI_ENCLOSURE_URL      = 5;
    private static final int    COLI_ENCLOSURE_TYPE     = 6;
    private static final int    COLI_PUBDATE            = 7;
    private static final int    COLI_LINK               = 8;

    private static final ColumnItem[] sQueryProjection = new ColumnItem[] {
            ColumnItem.ID, // Mandatory.
            ColumnItem.CHANNELID,
            ColumnItem.TITLE,
            ColumnItem.DESCRIPTION,
            ColumnItem.ENCLOSURE_LENGTH,
            ColumnItem.ENCLOSURE_URL,
            ColumnItem.ENCLOSURE_TYPE,
            ColumnItem.PUBDATE,
            ColumnItem.LINK };

    private final DBPolicy  mDbp = DBPolicy.get();
    private final ContentsManager mCm = ContentsManager.get();
    private final RTTask    mRtt = RTTask.get();
    private final int       mAppWidgetId;
    private final DBWatcher         mDbWatcher;
    private final ItemActionHandler mItemAction;

    private long    mCategoryId;
    private long[]  mCids = null;
    private Cursor  mCursor = null;
    private final Object mCursorLock = new Object();

    private class DBWatcher implements ListenerManager.Listener {
        private final HashSet<Long> _mCidSet = new HashSet<Long>();

        // See comment at 'onNotify'
        private AtomicReference<Boolean> _mClosed = new AtomicReference<Boolean>(false);

        private boolean
        isInCategory(Long cid) {
            if (null == cid)
                return false;
            return _mCidSet.contains(cid);
        }

        private boolean
        isInCategoryItem(long id) {
            Long cid = mDbp.getItemInfoLong(id, ColumnItem.CHANNELID);
            return isInCategory(cid);
        }

        void
        register() {
            mDbp.registerUpdatedListener(this, DB.UpdateType.CHANNEL_DATA.flag()
                                               | DB.UpdateType.CHANNEL_TABLE.flag());
            mDbp.registerChannelUpdatedListener(this, this);
            mCm.registerUpdatedListener(this, ContentsManager.UpdateType.ITEM_DATA.flag()
                                              | ContentsManager.UpdateType.CHAN_DATA.flag());

        }

        void
        close() {
            _mClosed.set(true);
        }

        void
        unregister() {
            mDbp.unregisterUpdatedListener(this);
            mDbp.unregisterChannelUpdatedListener(this);
            mCm.unregisterUpdatedListener(this);
        }

        void
        updateCategoryChannels(long[] cids) {
            _mCidSet.clear();
            for (long cid : cids)
                _mCidSet.add(cid);
        }

        @Override
        public void
        onNotify(Object user, ListenerManager.Type type, Object arg0, Object arg1) {
            // NOTE
            // unregister is called at UI context after onDestroy by 'post'.
            // So, following case is possible.
            // 1. onDestroy is called. And ViewsFactory is destroyed.
            //    (member variables of ViewsFactory are unavailable)
            // 2. Before 'unregister' is called at UI context, 'onNotify' is called.
            // 3. Accessing to member variables of ViewsFactory leads to unexpected results.
            //
            // To avoid this, _mClosed flag is used.
            if (_mClosed.get())
                return;

            if (type instanceof DB.UpdateType) {
                switch ((DB.UpdateType)type) {
                case CHANNEL_DATA:
                    long cid = (Long)arg0;
                    // Check that number of channels in the category is changed.
                    long catid = mDbp.getChannelInfoLong(cid, ColumnChannel.CATEGORYID);
                    if (DBG) P.v("Channel update : " + ((DB.UpdateType)type).name() + " : "
                                                 + mAppWidgetId + ", "
                                                 +  mCategoryId + ", " + catid + ", " + cid);
                    if ((!isInCategory(cid) && catid == mCategoryId) // channel is newly inserted to this category.
                        || (isInCategory(cid) && catid != mCategoryId)) { // channel is removed from this category.
                        refreshItemList();
                    }
                    break;

                case CHANNEL_TABLE:
                    // Channel may be deleted or newly inserted.
                    // So, refresh item list forcely...
                    refreshItemList();
                    break;

                default:
                    eAssert(false);
                }
            } else if (type instanceof DBPolicy.UpdateType) {
                switch ((DBPolicy.UpdateType)type) {
                case NEW_ITEMS: {
                    long cid = (Long)arg0;
                    int nrNewItems = (Integer)arg1;
                    if (isInCategory(cid)
                            && nrNewItems > 0)
                            refreshItemList();
                } break;

                case LAST_ITEM_ID:
                    break; // ignore

                default:
                    assert(false);
                }
            } else if (type instanceof ContentsManager.UpdateType) {
                switch ((ContentsManager.UpdateType)type) {
                case ITEM_DATA:
                    for (long id : (long[])arg0) {
                        if (isInCategoryItem(id)) {
                            refreshItemList();
                            break;
                        }
                    }
                    break;

                case CHAN_DATA:
                    boolean in = false;
                    for (long cid : (long[])arg0) {
                        if (isInCategory(cid)) {
                            refreshItemList();
                            break;
                        }
                    }
                    break;
                }
            }
        }
    }

    private class RTTaskRegisterListener implements RTTask.OnRegisterListener {
        @Override
        public void
        onRegister(BGTask task, long id, RTTask.Action act) {
            if (RTTask.Action.DOWNLOAD == act
                && isWidgetItem(id))
                mRtt.bind(id, act, ViewsFactory.this, new DownloadDataBGTaskListener(id));
        }

        @Override
        public void
        onUnregister(BGTask task, long id, RTTask.Action act) { }
    }

    private class DownloadDataBGTaskListener extends BaseBGTask.OnEventListener {
        private long _mId = -1;
        DownloadDataBGTaskListener(long id) {
            _mId = id;
        }

        @Override
        public void
        onCancelled(BaseBGTask task, Object param) {
            notifyDataSetChanged();
        }

        @Override
        public void
        onPreRun(BaseBGTask task) {
            // icon should be changed from 'ready' to 'running'
            notifyDataSetChanged();
        }

        @Override
        public void
        onPostRun(BaseBGTask task, Err result) {
            notifyDataSetChanged();
        }
    }

    private class AdapterBridge implements ItemActionHandler.AdapterBridge {
        @Override
        public void
        updateItemState(int pos, long state) {
            // do nothing.
        }

        @Override
        public void
        dataSetChanged(long id) {
            notifyDataSetChanged();
        }
    }

    private Cursor
    getCursor() {
        mCids = mDbp.getChannelIds(mCategoryId);
        mDbWatcher.updateCategoryChannels(mCids);
        if (DBG) P.v("Channels : " + Utils.nrsToNString(mCids));
        return mDbp.queryItem(mCids, sQueryProjection);
    }

    private boolean
    isWidgetChannel(long cid) {
        for (long i : mCids) {
            if (i == cid)
                return true;
        }
        return false;
    }

    private boolean
    isWidgetItem(long id) {
        long cid = mDbp.getItemInfoLong(id, ColumnItem.CHANNELID);
        return isWidgetChannel(cid);
    }

    private void
    notifyDataSetChanged() {
        AppWidgetManager awm = AppWidgetManager.getInstance(Environ.getAppContext());
        awm.notifyAppWidgetViewDataChanged(mAppWidgetId, R.id.list);
    }

    private void
    refreshItemList() {
        if (DBG) P.v("WidgetDataChanged : " + mAppWidgetId);
        Cursor newCur = getCursor();
        Cursor cur = mCursor;
        synchronized (mCursorLock) {
            mCursor = newCur;
        }
        notifyDataSetChanged();
        if (null != cur)
            cur.close();
    }

    public ViewsFactory(long categoryId, int appWidgetId) {
        mCategoryId = categoryId;
        mAppWidgetId = appWidgetId;

        mDbWatcher = new DBWatcher();
        mDbWatcher.register();

        Cursor cur = getCursor();
        // Doing time-consuming job in advance.
        cur.getCount();
        cur.moveToFirst();
        mCursor = cur;

        mItemAction = new ItemActionHandler(null, new AdapterBridge());
        mRtt.registerRegisterEventListener(this, new RTTaskRegisterListener());
    }

    boolean
    setCategory(long categoryId) {
        if (mCategoryId == categoryId)
            return false; // nothing to do
        if (DBG) P.d("Category Updated : " + mCategoryId + " -> " + categoryId);
        mCategoryId = categoryId;
        refreshItemList();
        return true;
    }

    long
    getCategory() {
        return mCategoryId;
    }

    void
    onItemClick(int position, long id) {
        eAssert(Utils.isUiThread());
        if (DBG) P.v("pos : " + position);
        Long cidLong = mDbp.getItemInfoLong(id, ColumnItem.CHANNELID);
        if (null == cidLong) {
            // CASE (reproducing step)
            //   - Channel is deleted
            //   - App widget is NOT updated yet.
            //   - User select ALREADY-DELETED-ITEM
            UiHelper.showTextToast(Environ.getAppContext(), R.string.warn_bad_request);
            return;
        }

        long cid = cidLong;
        long act = mDbp.getChannelInfoLong(cid, ColumnChannel.ACTION);
        String[] strs = mDbp.getItemInfoStrings(id, new ColumnItem[] { ColumnItem.LINK,
                                                                       ColumnItem.ENCLOSURE_URL,
                                                                       ColumnItem.ENCLOSURE_TYPE });
        mItemAction.onAction(act,
                             id,
                             position,
                             strs[0], // link
                             strs[1], // enclosure
                             strs[2]);// type.
    }

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

    @Override
    public int
    getCount() {
        synchronized (mCursorLock) {
            return mCursor.getCount();
        }
    }

    @Override
    public long
    getItemId(int position) {
        synchronized (mCursorLock) {
            // Called at binder thread
            mCursor.moveToPosition(position);
            return mCursor.getLong(COLI_ID);
        }
    }

    @Override
    public RemoteViews
    getLoadingView() {
        // Called at binder thread
        // TODO
        // Loading view here.
        if (DBG) P.v("Enter");
        return null;
    }

    @Override
    public RemoteViews
    getViewAt(int position) {
        // Called at binder thread
        long iid, cid;
        String title, desc;
        synchronized (mCursorLock) {
            mCursor.moveToPosition(position);
            iid = mCursor.getLong(COLI_ID);
            cid = mCursor.getLong(COLI_CHANNELID);
            title = mCursor.getString(COLI_TITLE);
            desc = mCursor.getString(COLI_DESCRIPTION);
        }
        RemoteViews rv = new RemoteViews(Environ.getAppContext().getPackageName(),
                                         R.layout.appwidget_row);
        rv.setTextViewText(R.id.channel, mDbp.getChannelInfoString(cid, ColumnChannel.TITLE));
        rv.setTextViewText(R.id.title, title);
        rv.setTextViewText(R.id.description, desc);


        File df = ContentsManager.get().getItemInfoDataFile(iid);
        if (DBG) P.v("pos : " + position
                     + " : Path(" + df.exists() + ") ["
                     + ((null == df)? "<null>": df.getAbsolutePath()) + "]");
        if (null != df && df.exists()) {
            rv.setViewVisibility(R.id.image, View.VISIBLE);
            rv.setImageViewResource(R.id.image, R.drawable.ic_save);
        } else {
            RTTask.TaskState dnState = mRtt.getState(iid, RTTask.Action.DOWNLOAD);
            rv.setViewVisibility(R.id.image, View.VISIBLE);
            switch(dnState) {
            case IDLE:
                rv.setViewVisibility(R.id.image, View.GONE);
                break;

            case READY:
                rv.setImageViewResource(R.id.image, R.drawable.ic_pause);
                break;

            case RUNNING:
                rv.setImageViewResource(R.id.image, R.drawable.ic_refresh);
                break;

            case FAILED:
                rv.setImageViewResource(R.id.image, R.drawable.ic_info);
                break;

            case CANCELING:
                rv.setImageViewResource(R.id.image, R.drawable.ic_block);
                break;

            default:
                eAssert(false);
            }
        }

        Intent ei = new Intent();
        ei.putExtra(AppWidgetUtils.MAP_KEY_POSITION, position);
        ei.putExtra(AppWidgetUtils.MAP_KEY_ITEMID, iid);
        rv.setOnClickFillInIntent(R.id.item_root, ei);
        return rv;
    }

    @Override
    public int
    getViewTypeCount() {
        // See http://developer.android.com/reference/android/widget/Adapter.html
        return 1;
    }

    @Override
    public boolean
    hasStableIds() {
        return true;
    }

    @Override
    public void
    onCreate() {
        if (DBG) P.v("widgetid : " + mAppWidgetId + " / " + mCategoryId);
    }

    @Override
    public void
    onDataSetChanged() {
        // Called at binder thread
    }

    @Override
    public void
    onDestroy() {
        // Call 'close()' to avoid race condition
        // See comments in 'onNotify' at DBWatcher
        mDbWatcher.close();
        // mDbWatcher.unregister SHOULD be called at UI context
        // See unregisterUpdatedListener at DB.java for details.
        Environ.getUiHandler().post(new Runnable() {
            @Override
            public void
            run() {
                mDbWatcher.unregister();
                mRtt.unregisterRegisterEventListener(this);
            }
        });
        if (DBG) P.v("Exit");
    }

    @Override
    protected void
    finalize() throws Throwable {
        super.finalize();
        if (null != mCursor)
            mCursor.close();
    }
}




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