au.com.wallaceit.reddinator.Rservice.java Source code

Java tutorial

Introduction

Here is the source code for au.com.wallaceit.reddinator.Rservice.java

Source

/*
 * Copyright 2013 Michael Boyde Wallace (http://wallaceit.com.au)
 * This file is part of Reddinator.
 *
 * Reddinator is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Reddinator is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Reddinator (COPYING). If not, see <http://www.gnu.org/licenses/>.
 */
package au.com.wallaceit.reddinator;

import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.os.Bundle;
import android.os.StrictMode;
import android.preference.PreferenceManager;
import android.text.Html;
import android.view.View;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
import android.widget.TextView;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

public class Rservice extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new ListRemoteViewsFactory(this.getApplicationContext(), intent);
    }
}

class ListRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
    private Context mContext = null;
    private int appWidgetId;
    private JSONArray data;
    private GlobalObjects global;
    private SharedPreferences mSharedPreferences;
    private String titleFontSize = "16";
    private int[] themeColors;
    private boolean loadCached = false; // tells the ondatasetchanged function that it should not download any further items, cache is loaded
    private boolean loadThumbnails = false;
    private boolean bigThumbs = false;
    private boolean hideInf = false;

    public ListRemoteViewsFactory(Context context, Intent intent) {
        this.mContext = context;
        appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);
        global = ((GlobalObjects) context.getApplicationContext());
        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
        //System.out.println("New view factory created for widget ID:"+appWidgetId);
        // Set thread network policy to prevent network on main thread exceptions.
        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
        StrictMode.setThreadPolicy(policy);
        // if this is a user request (apart from 'loadmore') or an auto update, do not attempt to load cache.
        // when a user clicks load more and a new view factory needs to be created we don't want to bypass cache, we want to load the cached items
        int loadType = global.getLoadType();
        if (!global.getBypassCache() || loadType == GlobalObjects.LOADTYPE_LOADMORE) {
            // load cached data
            data = global.getFeed(mSharedPreferences, appWidgetId);
            if (data.length() != 0) {
                titleFontSize = mSharedPreferences.getString(context.getString(R.string.widget_theme_pref), "16");
                try {
                    lastItemId = data.getJSONObject(data.length() - 1).getJSONObject("data").getString("name");
                } catch (JSONException e) {
                    lastItemId = "0"; // Could not get last item ID; perform a reload next time and show error view :(
                    e.printStackTrace();
                }
                if (loadType == GlobalObjects.LOADTYPE_LOAD) {
                    loadCached = true; // this isn't a loadmore request, the cache is loaded and we're done
                    //System.out.println("Cache loaded, no user request received.");
                }
            } else {
                loadReddits(false); // No feed items; do a reload.
            }
        } else {
            data = new JSONArray(); // set empty data to prevent any NPE
        }
    }

    @Override
    public void onCreate() {
        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
        StrictMode.setThreadPolicy(policy);
        endOfFeed = false;
        // get thumbnail load preference for the widget
        loadThumbnails = mSharedPreferences.getBoolean("thumbnails-" + appWidgetId, true);
        bigThumbs = mSharedPreferences.getBoolean("bigthumbs-" + appWidgetId, false);
        hideInf = mSharedPreferences.getBoolean("hideinf-" + appWidgetId, false);
    }

    @Override
    public void onDestroy() {
        // no-op
        //System.out.println("Service detroyed");
    }

    @Override
    public int getCount() {
        return (data.length() + 1); // plus 1 advertises the "load more" item to the listview without having to add it to the data source
    }

    @Override
    public RemoteViews getViewAt(int position) {
        RemoteViews row;
        if (position > data.length()) {
            return null; //  prevent errornous views
        }
        // check if its the last view and return loading view instead of normal row
        if (position == data.length()) {
            // build load more item
            //System.out.println("load more getViewAt("+position+") firing");
            RemoteViews loadmorerow = new RemoteViews(mContext.getPackageName(), R.layout.listrowloadmore);
            if (endOfFeed) {
                loadmorerow.setTextViewText(R.id.loadmoretxt, "There's nothing more here");
            } else {
                loadmorerow.setTextViewText(R.id.loadmoretxt, "Load more...");
            }
            loadmorerow.setTextColor(R.id.loadmoretxt, themeColors[0]);
            Intent i = new Intent();
            Bundle extras = new Bundle();
            extras.putString(WidgetProvider.ITEM_ID, "0"); // zero will be an indicator in the onreceive function of widget provider if its not present it forces a reload
            i.putExtras(extras);
            loadmorerow.setOnClickFillInIntent(R.id.listrowloadmore, i);
            return loadmorerow;
        } else {
            // build normal item
            String title = "";
            String url = "";
            String permalink = "";
            String thumbnail = "";
            String domain = "";
            String id = "";
            int score = 0;
            int numcomments = 0;
            boolean nsfw = false;
            try {
                JSONObject tempobj = data.getJSONObject(position).getJSONObject("data");
                title = tempobj.getString("title");
                //userlikes = tempobj.getString("likes");
                domain = tempobj.getString("domain");
                id = tempobj.getString("name");
                url = tempobj.getString("url");
                permalink = tempobj.getString("permalink");
                thumbnail = (String) tempobj.get("thumbnail"); // we have to call get and cast cause its not in quotes
                score = tempobj.getInt("score");
                numcomments = tempobj.getInt("num_comments");
                nsfw = tempobj.getBoolean("over_18");
            } catch (JSONException e) {
                e.printStackTrace();
                // return null; // The view is invalid;
            }
            // create remote view from specified layout
            if (bigThumbs) {
                row = new RemoteViews(mContext.getPackageName(), R.layout.listrowbigthumb);
            } else {
                row = new RemoteViews(mContext.getPackageName(), R.layout.listrow);
            }
            // build view
            row.setTextViewText(R.id.listheading, Html.fromHtml(title).toString());
            row.setFloat(R.id.listheading, "setTextSize", Integer.valueOf(titleFontSize)); // use for compatibility setTextViewTextSize only introduced in API 16
            row.setTextColor(R.id.listheading, themeColors[0]);
            row.setTextViewText(R.id.sourcetxt, domain);
            row.setTextColor(R.id.sourcetxt, themeColors[3]);
            row.setTextColor(R.id.votestxt, themeColors[4]);
            row.setTextColor(R.id.commentstxt, themeColors[4]);
            row.setTextViewText(R.id.votestxt, String.valueOf(score));
            row.setTextViewText(R.id.commentstxt, String.valueOf(numcomments));
            row.setInt(R.id.listdivider, "setBackgroundColor", themeColors[2]);
            row.setViewVisibility(R.id.nsfwflag, nsfw ? TextView.VISIBLE : TextView.GONE);
            // add extras and set click intent
            Intent i = new Intent();
            Bundle extras = new Bundle();
            extras.putString(WidgetProvider.ITEM_ID, id);
            extras.putInt("itemposition", position);
            extras.putString(WidgetProvider.ITEM_URL, url);
            extras.putString(WidgetProvider.ITEM_PERMALINK, permalink);
            i.putExtras(extras);
            row.setOnClickFillInIntent(R.id.listrow, i);
            // load thumbnail if they are enabled for this widget
            if (loadThumbnails) {
                // load big image if preference is set
                if (!thumbnail.equals("")) { // check for thumbnail; self is used to display the thinking logo on the reddit site, we'll just show nothing for now
                    if (thumbnail.equals("nsfw") || thumbnail.equals("self") || thumbnail.equals("default")) {
                        int resource = 0;
                        switch (thumbnail) {
                        case "nsfw":
                            resource = R.drawable.nsfw;
                            break;
                        case "default":
                        case "self":
                            resource = R.drawable.self_default;
                            break;
                        }
                        row.setImageViewResource(R.id.thumbnail, resource);
                        row.setViewVisibility(R.id.thumbnail, View.VISIBLE);
                        //System.out.println("Loading default image: "+thumbnail);
                    } else {
                        Bitmap bitmap;
                        String fileurl = mContext.getCacheDir() + "/thumbcache-" + appWidgetId + "/" + id + ".png";
                        // check if the image is in cache
                        if (new File(fileurl).exists()) {
                            bitmap = BitmapFactory.decodeFile(fileurl);
                            saveImageToStorage(bitmap, id);
                        } else {
                            // download the image
                            bitmap = loadImage(thumbnail);
                        }
                        if (bitmap != null) {
                            row.setImageViewBitmap(R.id.thumbnail, bitmap);
                            row.setViewVisibility(R.id.thumbnail, View.VISIBLE);
                        } else {
                            // row.setImageViewResource(R.id.thumbnail, android.R.drawable.stat_notify_error); for later
                            row.setViewVisibility(R.id.thumbnail, View.GONE);
                        }
                    }
                } else {
                    row.setViewVisibility(R.id.thumbnail, View.GONE);
                }
            } else {
                row.setViewVisibility(R.id.thumbnail, View.GONE);
            }
            // hide info bar if options set
            if (hideInf) {
                row.setViewVisibility(R.id.infbox, View.GONE);
            } else {
                row.setViewVisibility(R.id.infbox, View.VISIBLE);
            }
        }
        //System.out.println("getViewAt("+position+");");
        return row;
    }

    private Bitmap loadImage(String urlstr) {
        URL url;
        Bitmap bmp;
        try {
            url = new URL(urlstr);
            URLConnection con = url.openConnection();
            con.setConnectTimeout(8000);
            con.setReadTimeout(8000);
            bmp = BitmapFactory.decodeStream(con.getInputStream());
        } catch (MalformedURLException e) {
            e.printStackTrace();
            return null;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
        return bmp;
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private boolean saveImageToStorage(Bitmap image, String redditid) {
        try {
            File file = new File(mContext.getCacheDir().getPath() + "/thumbcache-" + appWidgetId + "/",
                    redditid + ".png");
            if (!file.getParentFile().exists()) {
                file.getParentFile().mkdirs();
            }
            FileOutputStream fos = new FileOutputStream(file);
            image.compress(Bitmap.CompressFormat.PNG, 100, fos);
            // 100 means no compression, the lower you go, the stronger the compression
            fos.close();
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    private void clearImageCache() {
        // delete all images in the cache folder.
        DeleteRecursive(new File(mContext.getCacheDir() + "/thumbcache-app"));
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private void DeleteRecursive(File fileOrDirectory) {

        if (fileOrDirectory.isDirectory())
            for (File child : fileOrDirectory.listFiles())
                DeleteRecursive(child);

        fileOrDirectory.delete();

    }

    @Override
    public RemoteViews getLoadingView() {
        RemoteViews rowload = new RemoteViews(mContext.getPackageName(), R.layout.listrowload);
        if (themeColors == null) {
            getThemeColors(); // prevents null pointer
        }
        rowload.setTextColor(R.id.listloadtxt, themeColors[0]);
        return rowload;
    }

    @Override
    public int getViewTypeCount() {
        return (3);
    }

    @Override
    public long getItemId(int position) {
        return (position);
    }

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

    @Override
    public void onDataSetChanged() {
        // get thumbnail load preference for the widget
        loadThumbnails = mSharedPreferences.getBoolean("thumbnails-" + appWidgetId, true);
        bigThumbs = mSharedPreferences.getBoolean("bigthumbs-" + appWidgetId, false);
        hideInf = mSharedPreferences.getBoolean("hideinf-" + appWidgetId, false);
        titleFontSize = mSharedPreferences.getString("titlefontpref", "16");
        getThemeColors(); // reset theme colors
        int loadType = global.getLoadType();
        if (!loadCached) {
            loadCached = (loadType == GlobalObjects.LOADTYPE_REFRESH_VIEW); // see if its just a call to refresh view and set var accordingly but only check it if load cached is not already set true in the above constructor
        }
        //System.out.println("Loading type "+loadtype);
        if (!loadCached) {
            // refresh data
            if (loadType == GlobalObjects.LOADTYPE_LOADMORE && !lastItemId.equals("0")) { // do not attempt a "loadmore" if we don't have a valid item ID; this would append items to the list, instead perform a full reload
                global.setLoad();
                loadMoreReddits();
            } else {
                //System.out.println("loadReddits();");
                loadReddits(false);
            }
            global.setBypassCache(false); // don't bypass the cache check the next time the service starts
        } else {
            loadCached = false;
            global.setLoad();
            // hide loader
            hideWidgetLoader(false, false); // don't go to top as the user is probably interacting with the list
        }

    }

    private String lastItemId = "0";
    private boolean endOfFeed = false;

    private void loadMoreReddits() {
        //System.out.println("loadMoreReddits();");
        loadReddits(true);
    }

    private void loadReddits(boolean loadMore) {
        String curFeed = mSharedPreferences.getString("currentfeed-" + appWidgetId, "technology");
        String sort = mSharedPreferences.getString("sort-" + appWidgetId, "hot");
        // Load more or initial load/reload?
        if (loadMore) {
            // fetch 25 more after current last item and append to the list
            JSONArray tempData = global.mRedditData.getRedditFeed(curFeed, sort, 25, lastItemId);
            if (!isError(tempData)) {
                if (tempData.length() == 0) {
                    endOfFeed = true;
                } else {
                    endOfFeed = false;
                    int i = 0;
                    while (i < tempData.length()) {
                        try {
                            data.put(tempData.get(i));
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                        i++;
                    }
                    // Save feed data
                    global.setFeed(mSharedPreferences, appWidgetId, data);
                }
            } else {
                hideWidgetLoader(false, true); // don't go to top of list and show error icon
                return;
            }
        } else {
            endOfFeed = false;
            // clear image cache
            clearImageCache();
            // reload feed
            int limit = Integer.valueOf(mSharedPreferences.getString("numitemloadpref", "25"));
            JSONArray tempArray;
            tempArray = global.mRedditData.getRedditFeed(curFeed, sort, limit, "0");
            // check if data is valid; if the getredditfeed function fails to create a connection it returns -1 in the first value of the array
            if (!isError(tempArray)) {
                data = tempArray;
                if (data.length() == 0) {
                    endOfFeed = true;
                }
                // Save feed data
                global.setFeed(mSharedPreferences, appWidgetId, data);
            } else {
                hideWidgetLoader(false, true); // don't go to top of list and show error icon
                return;
            }
        }
        // set last item id for "loadmore use"
        // Damn reddit doesn't allow you to specify a start index for the data, instead you have to reference the last item id from the prev page :(
        try {
            lastItemId = data.getJSONObject(data.length() - 1).getJSONObject("data").getString("name"); // name is actually the unique id we want
        } catch (JSONException e) {
            lastItemId = "0"; // Could not get last item ID; perform a reload next time and show error view :(
            e.printStackTrace();
        }

        // hide loader
        if (loadMore) {
            hideWidgetLoader(false, false); // don't go to top of list
        } else {
            hideWidgetLoader(true, false); // go to top
        }
    }

    // check if the array is an error array
    private boolean isError(JSONArray tempArray) {
        boolean error;
        if (tempArray == null) {
            return true; // null error
        }
        if (tempArray.length() > 0) {
            try {
                error = tempArray.getString(0).equals("-1");
            } catch (JSONException e) {
                error = true;
                e.printStackTrace();
            }
        } else {
            error = false; // empty array means no more feed items
        }
        return error;
    }

    // hide appwidget loader
    private void hideWidgetLoader(boolean goToTopOfList, boolean showError) {
        AppWidgetManager mgr = AppWidgetManager.getInstance(mContext);
        // get theme layout id
        int layout = 1;
        switch (Integer.valueOf(mSharedPreferences.getString("widgetthemepref", "1"))) {
        case 1:
            layout = R.layout.widgetmain;
            break;
        case 2:
            layout = R.layout.widgetdark;
            break;
        case 3:
            layout = R.layout.widgetholo;
            break;
        case 4:
            layout = R.layout.widgetdarkholo;
            break;
        case 5:
            layout = R.layout.widgettrans;
            break;
        }
        RemoteViews views = new RemoteViews(mContext.getPackageName(), layout);
        views.setViewVisibility(R.id.srloader, View.INVISIBLE);
        // go to the top of the list view
        if (goToTopOfList) {
            views.setScrollPosition(R.id.listview, 0);
        }
        if (showError) {
            views.setViewVisibility(R.id.erroricon, View.VISIBLE);
        }
        mgr.partiallyUpdateAppWidget(appWidgetId, views);
    }

    private void getThemeColors() {
        switch (Integer.valueOf(mSharedPreferences.getString("widgetthemepref", "1"))) {
        // set colors array: healine text, load more text, divider, domain text, vote & comments
        case 1:
            themeColors = new int[] { Color.BLACK, Color.BLACK, Color.parseColor("#D7D7D7"),
                    Color.parseColor("#336699"), Color.parseColor("#FF4500") };
            break;
        case 2:
            themeColors = new int[] { Color.WHITE, Color.WHITE, Color.parseColor("#646464"),
                    Color.parseColor("#5F99CF"), Color.parseColor("#FF8B60") };
            break;
        case 3:
        case 4:
        case 5:
            themeColors = new int[] { Color.WHITE, Color.WHITE, Color.parseColor("#646464"),
                    Color.parseColor("#CEE3F8"), Color.parseColor("#FF8B60") };
            break;
        }
        // user title color override
        if (!mSharedPreferences.getString("titlecolorpref", "0").equals("0")) {
            themeColors[0] = Color.parseColor(mSharedPreferences.getString("titlecolorpref", "#000"));
        }
    }
}