com.appsimobile.appsii.module.home.HomeAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.appsimobile.appsii.module.home.HomeAdapter.java

Source

/*
 * Copyright 2015. Appsi Mobile
 *
 * 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.appsimobile.appsii.module.home;

import android.Manifest;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProviderInfo;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.ContentProviderOperation;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.OperationApplicationException;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.Settings;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.util.SimpleArrayMap;
import android.support.v7.graphics.Palette;
import android.support.v7.widget.CardView;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.format.Time;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Advanceable;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.TextView;

import com.appsimobile.appsii.AnalyticsManager;
import com.appsimobile.appsii.BitmapUtils;
import com.appsimobile.appsii.DrawableCompat;
import com.appsimobile.appsii.DrawableStartTintPainter;
import com.appsimobile.appsii.PermissionDeniedException;
import com.appsimobile.appsii.PopupMenuHelper;
import com.appsimobile.appsii.R;
import com.appsimobile.appsii.annotation.VisibleForTesting;
import com.appsimobile.appsii.appwidget.AppWidgetUtils;
import com.appsimobile.appsii.appwidget.AppsiiAppWidgetHost;
import com.appsimobile.appsii.dagger.AppInjector;
import com.appsimobile.appsii.module.home.appwidget.WidgetChooserActivity;
import com.appsimobile.appsii.module.home.config.HomeItemConfiguration;
import com.appsimobile.appsii.module.home.config.HomeItemConfigurationHelper;
import com.appsimobile.appsii.module.home.provider.HomeContract;
import com.appsimobile.appsii.module.weather.WeatherUtils;
import com.appsimobile.appsii.module.weather.loader.WeatherData;
import com.appsimobile.appsii.permissions.PermissionUtils;
import com.appsimobile.paintjob.PaintJob;
import com.appsimobile.paintjob.ViewPainters;
import com.crashlytics.android.Crashlytics;

import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
import java.util.TimeZone;

import javax.inject.Inject;

/**
 * The adapter used on the Home-Page and in the HomeEditorActivity.
 * Created by nick on 20/01/15.
 */
public class HomeAdapter extends RecyclerView.Adapter<AbsHomeViewHolder> {

    static final String PERMISSION_ERROR_LOCATION_WEATHER = "_location_weather_";

    static final String PERMISSION_ERROR_CONTACTS_PROFILE = "_contact_profile_image_";

    /**
     * A place to support very basic caching of the host-views. The cache is automatically
     * cleared when the host stops listening.
     */
    static final SparseArray<AppWidgetHostView> sAppWidgetViewCache = new SparseArray<>();

    /**
     * The list of home items in the adapter.
     */
    final ArrayList<HomeItem> mHomeItems = new ArrayList<>(24);

    /**
     * A cached layout-inflater to prevent having to get it all the time
     */
    final LayoutInflater mLayoutInflater;

    /**
     * The context to which we are bound
     */
    final Context mContext;

    /**
     * The factory to create the view-wrappers. Can be overridden to create
     * a custom view-wrapper.
     */
    final ViewWrapperFactory mViewWrapperFactory;

    /**
     * The internal lookup for the home-spans
     */
    final HomeSpanSizeLookup mHomeSpanSizeLookup;

    /**
     * The id of the home-page we are bound to (or editing for)
     */
    final long mHomeId;
    /**
     * A helper that auto-advances the views that need to be auto-advanced
     */
    final AutoAdvanceHelper mAutoAdvanceHelper = new AutoAdvanceHelper();
    /**
     * True in case app widgets must be enabled. false otherwise. Setting this
     * to false, will use a different view-holder that only loads the preview
     * image instead of the normal widget view.
     */
    final boolean mAppWidgetsEnabled;
    /**
     * The base height of the widgets. This is set to 72dp in the constructor
     */
    final int mBaseHeight;
    /**
     * The height to increase the widget height with, for each step.
     */
    final int mHeightPerStep;
    /**
     * The app-widget-host. Can be null in case mAppWidgetsEnabled is false.
     */
    @Inject
    AppsiiAppWidgetHost mAppWidgetHost;
    /**
     * The app-widget manager. Used by widget view-holders to load the
     * AppWidgetInfo for the bound app-widget. In the disable variant it
     * is used to load the image.
     */
    @Inject
    AppWidgetManager mAppWidgetManager;
    @Inject
    HomeItemConfiguration mHomeItemConfiguration;
    @Inject
    PermissionUtils mPermissionUtils;
    @Inject
    WeatherUtils mWeatherUtils;
    @Inject
    AppWidgetUtils mAppWidgetUtils;
    long mUnsetId = -1L;

    /**
     * True if the home-adapter is started. This specifically impacts the widgets
     * which are started and stopped when this state changes.
     */
    boolean mStarted;

    PermissionErrorListener mPermissionErrorListener;

    @Inject
    AnalyticsManager mAnalyticsManager;

    public HomeAdapter(Context context, long homeId) {
        this(context, new DefaultViewWrapperFactory(), homeId, true);
    }

    public HomeAdapter(Context context, ViewWrapperFactory viewWrapperFactory, long homeId,
            boolean appWidgetsEnabled) {
        mContext = context;
        mHomeId = homeId;
        mAppWidgetsEnabled = appWidgetsEnabled;

        mViewWrapperFactory = viewWrapperFactory;
        mBaseHeight = (int) (context.getResources().getDisplayMetrics().density * 72);
        mHeightPerStep = (int) (context.getResources().getDisplayMetrics().density * 24);
        mLayoutInflater = LayoutInflater.from(context);
        mHomeSpanSizeLookup = new HomeSpanSizeLookup(context);

        setHasStableIds(true);
        AppInjector.inject(this);
    }

    long nextUnsetId() {
        return mUnsetId--;
    }

    public HomeItem getItemAt(int position) {
        return mHomeItems.get(position);
    }

    public void getItemsInRow(long rowId, List<HomeItem> homeItems) {
        int N = mHomeItems.size();
        for (int i = 0; i < N; i++) {
            HomeItem homeItem = mHomeItems.get(i);
            if (homeItem.mRowId == rowId) {
                homeItems.add(homeItem);
            }
        }
    }

    @Override
    public AbsHomeViewHolder onCreateViewHolder(ViewGroup parent, int encodedViewType) {
        int viewType = encodedViewType & 0x0000FFFF;
        int rowHeight = (encodedViewType & 0xFFFF0000) >> 16;

        int heightPx = heightForRowHeight(rowHeight);

        switch (viewType) {
        case HomeContract.Cells.DISPLAY_TYPE_UNSET:
            return createEmptyViewHolder(heightPx);
        case HomeContract.Cells.DISPLAY_TYPE_WEATHER_TEMP:
            return createPlainTemperatureViewHolder(parent, heightPx);
        case HomeContract.Cells.DISPLAY_TYPE_WEATHER_TEMP_WALLPAPER:
            return createWallpaperTemperatureViewHolder(parent, heightPx);
        case HomeContract.Cells.DISPLAY_TYPE_WEATHER_WIND_WALLPAPER:
            //                return createWallpaperWindViewHolder(parent, heightPx);
        case HomeContract.Cells.DISPLAY_TYPE_WEATHER_WIND:
            return createPlainWindViewHolder(parent, heightPx);
        case HomeContract.Cells.DISPLAY_TYPE_WEATHER_SUNRISE_WALLPAPER:
            //                return createWallpaperSunViewHolder(parent, heightPx);
        case HomeContract.Cells.DISPLAY_TYPE_WEATHER_SUNRISE:
            return createPlainSunViewHolder(parent, heightPx);
        case HomeContract.Cells.DISPLAY_TYPE_CLOCK:
            return createClockViewHolder(parent, heightPx);
        case HomeContract.Cells.DISPLAY_TYPE_BLUETOOTH_TOGGLE:
            return createBluetoothViewHolder(parent, heightPx);
        case HomeContract.Cells.DISPLAY_TYPE_INTENT:
            return createIntentTypeViewHolder(parent, heightPx);
        case HomeContract.Cells.DISPLAY_TYPE_PROFILE_IMAGE:
            return createProfileImageViewHolder(parent, heightPx);
        case HomeContract.Cells.DISPLAY_TYPE_APP_WIDGET:
            // for widgets, when they are not enabled, use a disabled
            // version of the viewholder.
            if (mAppWidgetsEnabled) {
                return createAppWidgetViewHolder(parent, heightPx);
            } else {
                return createDisabledAppWidgetViewHolder(parent, heightPx);

            }
        }
        return createDummyViewHolder(heightPx);
    }

    int heightForRowHeight(int rowHeight) {
        return mBaseHeight + (mHeightPerStep * rowHeight);
    }

    private AbsHomeViewHolder createEmptyViewHolder(int height) {
        View result = new View(mContext);
        HomeViewWrapper wrapped = wrapView(mContext, result, height);
        return new EmptyViewViewHolder(wrapped);
    }

    private AbsHomeViewHolder createPlainTemperatureViewHolder(ViewGroup parent, int height) {
        View view = mLayoutInflater.inflate(R.layout.home_item_weather, parent, false);
        HomeViewWrapper wrapped = wrapView(mContext, view, height);
        return new WeatherTemperaturePlainViewHolder(wrapped);
    }

    private AbsHomeViewHolder createWallpaperTemperatureViewHolder(ViewGroup parent, int height) {
        View view = mLayoutInflater.inflate(R.layout.home_item_weather_wp, parent, false);
        HomeViewWrapper wrapped = wrapView(mContext, view, height);
        return new WeatherTemperatureWallpaperViewHolder(wrapped);
    }

    private AbsHomeViewHolder createPlainWindViewHolder(ViewGroup parent, int height) {
        View view = mLayoutInflater.inflate(R.layout.home_item_weather_wind, parent, false);
        HomeViewWrapper wrapped = wrapView(mContext, view, height);
        return new PlainWeatherWindViewHolder(wrapped);
    }

    private AbsHomeViewHolder createPlainSunViewHolder(ViewGroup parent, int height) {
        View view = mLayoutInflater.inflate(R.layout.home_item_weather_sun, parent, false);
        HomeViewWrapper wrapped = wrapView(mContext, view, height);
        return new PlainWeatherSunViewHolder(wrapped);
    }

    private AbsHomeViewHolder createClockViewHolder(ViewGroup parent, int height) {
        View view = mLayoutInflater.inflate(R.layout.home_item_analog_clock, parent, false);
        HomeViewWrapper wrapped = wrapView(mContext, view, height);
        return new ClockViewHolder(wrapped, mHomeItemConfiguration);
    }

    private AbsHomeViewHolder createBluetoothViewHolder(ViewGroup parent, int height) {
        View view = mLayoutInflater.inflate(R.layout.home_item_bluetooth_toggle, parent, false);
        HomeViewWrapper wrapped = wrapView(mContext, view, height);
        return new BluetoothViewHolder(wrapped, mHomeItemConfiguration);
    }

    private AbsHomeViewHolder createIntentTypeViewHolder(ViewGroup parent, int height) {
        View view = mLayoutInflater.inflate(R.layout.home_item_app, parent, false);
        HomeViewWrapper wrapped = wrapView(mContext, view, height);
        return new IntentViewHolder(wrapped);
    }

    private AbsHomeViewHolder createProfileImageViewHolder(ViewGroup parent, int height) {
        View view = mLayoutInflater.inflate(R.layout.home_item_image, parent, false);
        HomeViewWrapper wrapped = wrapView(mContext, view, height);
        return new ProfileImageViewHolder(wrapped);
    }

    private AbsHomeViewHolder createAppWidgetViewHolder(ViewGroup parent, int height) {
        View view = mLayoutInflater.inflate(R.layout.home_item_appwidget, parent, false);
        HomeViewWrapper wrapped = wrapView(mContext, view, height);
        AppWidgetViewHolder holder = new AppWidgetViewHolder(wrapped, mAppWidgetHost, mAppWidgetManager,
                mAutoAdvanceHelper);
        holder.postCreate();
        return holder;
    }

    private AbsHomeViewHolder createDisabledAppWidgetViewHolder(ViewGroup parent, int height) {
        View view = mLayoutInflater.inflate(R.layout.home_item_appwidget_disabled, parent, false);
        HomeViewWrapper wrapped = wrapView(mContext, view, height);
        return new DisabledAppWidgetViewHolder(wrapped, mAppWidgetManager, mAppWidgetUtils);
    }

    private AbsHomeViewHolder createDummyViewHolder(int height) {
        TextView result = new TextView(mContext);
        HomeViewWrapper wrapped = wrapView(mContext, result, height);
        return new SimpleTextViewViewHolder(wrapped);
    }

    HomeViewWrapper wrapView(Context context, View child, int heightPx) {
        return mViewWrapperFactory.wrapView(context, child, heightPx);
    }

    @Override
    public void onBindViewHolder(AbsHomeViewHolder absHomeViewHolder, int i) {
        HomeItem item = mHomeItems.get(i);
        int heightPx = heightForRowHeight(item.mRowHeight);

        absHomeViewHolder.bind(item, heightPx);
    }

    @Override
    public int getItemViewType(int position) {
        HomeItem homeItem = mHomeItems.get(position);
        return (homeItem.mDisplayType & 0x0000FFFF) | (homeItem.mRowHeight << 16);
    }

    @Override
    public long getItemId(int position) {
        if (position < 0)
            return RecyclerView.NO_ID;
        if (position >= mHomeItems.size())
            return RecyclerView.NO_ID;

        return mHomeItems.get(position).mId;
    }

    @Override
    public int getItemCount() {
        return mHomeItems.size();
    }

    @Override
    public void onViewRecycled(AbsHomeViewHolder holder) {
        super.onViewRecycled(holder);
        if (holder instanceof AbsWeatherWindViewHolder) {
            ((AbsWeatherWindViewHolder) holder).onRecycled();
        }
    }

    private AbsHomeViewHolder createWallpaperWindViewHolder(ViewGroup parent, int height) {
        View view = mLayoutInflater.inflate(R.layout.home_item_weather_wind, parent, false);
        HomeViewWrapper wrapped = wrapView(mContext, view, height);
        return new WallpaperWeatherWindViewHolder(wrapped);
    }

    private AbsHomeViewHolder createWallpaperSunViewHolder(ViewGroup parent, int height) {
        View view = mLayoutInflater.inflate(R.layout.home_item_weather_sun_wp, parent, false);
        HomeViewWrapper wrapped = wrapView(mContext, view, height);
        return new WallpaperWeatherSunViewHolder(wrapped);
    }

    public void setHomeItems(List<HomeItem> data) {
        mHomeItems.clear();
        int N = data.size();
        for (int i = 0; i < N; i++) {
            HomeItem item = data.get(i);
            if (item.mPageId == mHomeId) {
                mHomeItems.add(item);
            }
        }
        try {
            mHomeSpanSizeLookup.setup(mHomeItems);
        } catch (InconsistentRowsException e) {
            fixHomeItemRows(mHomeItems);
        }
        notifyDataSetChanged();
    }

    private void fixHomeItemRows(List<HomeItem> homeItems) {
        int lastRowPosition = -1;
        int newRowIdx = 0;
        int N = homeItems.size();

        ArrayList<ContentProviderOperation> ops = new ArrayList<>(8);

        boolean done;

        for (int i = 0; i < N; i++) {
            HomeItem item = homeItems.get(i);
            done = false;
            if (lastRowPosition == -1 || lastRowPosition != item.mRowPosition) {
                lastRowPosition = item.mRowPosition;

                Uri uri = ContentUris.withAppendedId(HomeContract.Rows.CONTENT_URI, item.mRowId);

                ops.add(ContentProviderOperation.newUpdate(uri).withValue(HomeContract.Rows.POSITION, newRowIdx)
                        .build());
                newRowIdx++;
                done = true;
            }
            // handle the last row
            if (i == mHomeItems.size() - 1 && !done) {
                Uri uri = ContentUris.withAppendedId(HomeContract.Rows.CONTENT_URI, item.mRowId);

                ops.add(ContentProviderOperation.newUpdate(uri).withValue(HomeContract.Rows.POSITION, newRowIdx)
                        .build());

            }
        }

        try {
            mContext.getContentResolver().applyBatch(HomeContract.AUTHORITY, ops);
        } catch (RemoteException | OperationApplicationException e) {
            Log.w("HomeAdapter", "error fixing bad row numbers", e);
        }

    }

    public HomeSpanSizeLookup getHomeSpanSizeLookup() {
        return mHomeSpanSizeLookup;
    }

    HomeAdapterEditor getEditor() {
        return new HomeAdapterEditor();
    }

    public int getPositionOfId(long lastId) {
        int N = mHomeItems.size();
        for (int i = 0; i < N; i++) {
            HomeItem item = mHomeItems.get(i);
            if (item.mId == lastId) {
                return i;
            }
        }
        return -1;
    }

    public void setStarted(boolean started) {
        if (mAppWidgetHost != null && mAppWidgetsEnabled) {
            if (mStarted != started) {
                mStarted = started;

                if (started) {
                    mAppWidgetHost.startListening();
                    mAutoAdvanceHelper.start();
                } else {
                    mAppWidgetHost.stopListening();
                    mAutoAdvanceHelper.stop();
                }
            }
        }

        if (mAppWidgetsEnabled) {
            if (!started) {
                sAppWidgetViewCache.clear();
            }
        }
    }

    public HomeItem getHomeItemForAppWidgetId(int appWidgetId) {
        HomeItemConfiguration helper = mHomeItemConfiguration;
        long cellId = helper.findCellWithPropertyValue("app_widget_id", String.valueOf(appWidgetId));

        if (cellId != -1) {
            int N = mHomeItems.size();
            for (int i = 0; i < N; i++) {
                HomeItem homeItem = mHomeItems.get(i);
                if (homeItem.mId == cellId) {
                    return homeItem;
                }
            }
        }
        return null;
    }

    public void onTrimMemory(int level) {
    }

    void onPermissionDenied(PermissionDeniedException e, String permission, String id) {
        // This allows the HomePage to implement something to handle this
        mPermissionErrorListener.onPermissionDenied(permission, id,
                R.string.permission_reason_contacts_profile_image);
    }

    public void setPermissionErrorListener(PermissionErrorListener permissionErrorListener) {
        mPermissionErrorListener = permissionErrorListener;
    }

    interface ViewWrapperFactory {

        /**
         * Wraps an home item view in a view that is used by the ViewHolder to manage the
         * height of the view in it's row.
         */
        HomeViewWrapper wrapView(Context context, View child, int heightPx);
    }

    interface OpsBuilder {

        Operation newInsert(Uri uri);

        Operation newDelete(Uri uri);

        Operation newUpdate(Uri uri);
    }

    interface Operation {

        Operation withValue(String position, int value);

        Operation withValue(String position, long value);

        Operation withValueBackReference(String rowId, int resultIdx);

        Operation withSelection(String selection, String[] selectionArgs);

        ContentProviderOperation build();

    }

    interface PermissionErrorListener {

        /**
         * Informs about a permission error.
         *
         * @param id The id of the error. This is unique for the combination
         * of the cell and the permission that was denied
         */
        void onPermissionDenied(String permission, String id, @StringRes int textResId);
    }

    static class DefaultViewWrapperFactory implements ViewWrapperFactory {

        @Override
        public HomeViewWrapper wrapView(Context context, View child, int heightPx) {
            HomeViewWrapper result = new HomeViewWrapper(context);
            RecyclerView.LayoutParams params = new RecyclerView.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
                    heightPx);

            result.addView(child, params);
            return result;
        }

    }

    static class ClockViewHolder extends BaseViewHolder implements View.OnClickListener,
            HomeItemConfigurationHelper.ConfigurationListener, PopupMenu.OnMenuItemClickListener {

        final AnalogClock mAnalogClock;

        final HomeItemConfiguration mConfigurationHelper;

        final TextView mTextView;

        public ClockViewHolder(HomeViewWrapper view, HomeItemConfiguration conf) {
            super(view);
            mConfigurationHelper = conf;
            mAnalogClock = (AnalogClock) view.findViewById(R.id.analog_clock);
            mTextView = (TextView) view.findViewById(R.id.primary_text);
            mOverflow.setOnClickListener(this);
        }

        @Override
        public void onClick(View v) {
            int id = v.getId();
            if (id == R.id.overflow) {
                showOverflowMenu(v);
            }

        }

        private void showOverflowMenu(View v) {
            PopupMenuHelper.showPopupMenu(v, R.menu.home_item_clock, this);
        }

        @Override
        public boolean onMenuItemClick(MenuItem item) {
            int itemId = item.getItemId();
            if (itemId == R.id.action_cell_weather_prefs) {

                Context context = itemView.getContext();
                Intent intent = new Intent(context, CellClockActivity.class);
                intent.putExtra(CellClockActivity.EXTRA_CELL_ID, mHomeItem.mId);
                context.startActivity(intent);
                return true;
            }
            return false;
        }

        @Override
        void updateConfiguration() {
            long cellId = mHomeItem.mId;
            String timezone = mConfigurationHelper.getProperty(cellId, "timezone_id", null);
            String title = mConfigurationHelper.getProperty(cellId, "title", null);

            if (timezone == null) {
                mAnalogClock.clearTimezone();
            } else {
                mAnalogClock.setTimezone(timezone);
            }
            if (TextUtils.isEmpty(title)) {
                mTextView.setText(R.string.no_title);
            } else {
                mTextView.setText(title);
            }
        }

        @Override
        void bind(HomeItem item, int heightPx) {
            super.bind(item, heightPx);
            mChildView.setOnClickListener(this);
            updateConfiguration();
        }

    }

    static class BluetoothViewHolder extends BaseViewHolder implements View.OnClickListener,
            HomeItemConfigurationHelper.ConfigurationListener, PopupMenu.OnMenuItemClickListener {

        final HomeItemConfiguration mConfigurationHelper;

        final TextView mTextView;

        final Drawable mBluetoothDrawable;

        final ImageView mImageView;

        private final int mPrimaryColor;

        private final int mWidgetColor;

        private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                final String action = intent.getAction();

                if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
                    final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
                    switch (state) {
                    case BluetoothAdapter.STATE_OFF:
                        showOffStatus();
                        break;
                    case BluetoothAdapter.STATE_TURNING_OFF:
                        mTextView.setText(R.string.turning_off);
                        break;
                    case BluetoothAdapter.STATE_ON:
                        showOnStatus();
                        break;
                    case BluetoothAdapter.STATE_TURNING_ON:
                        mTextView.setText(R.string.turning_on);
                        break;
                    }
                }
            }
        };

        private final BluetoothAdapter mBluetoothAdapter;

        public BluetoothViewHolder(HomeViewWrapper view, HomeItemConfiguration conf) {
            super(view);
            mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
            mConfigurationHelper = conf;
            mImageView = (ImageView) view.findViewById(R.id.bluetooth_image);
            mBluetoothDrawable = mImageView.getDrawable();
            mTextView = (TextView) view.findViewById(R.id.primary_text);

            Context context = view.getContext();
            final TypedArray a = context.obtainStyledAttributes(new int[] { R.attr.colorPrimary, R.attr.colorAccent,
                    R.attr.colorPrimaryDark, R.attr.appsiHomeWidgetPrimaryColor, });

            mPrimaryColor = a.getColor(0, Color.BLACK);
            mWidgetColor = a.getColor(3, Color.BLACK);
            a.recycle();

            mOverflow.setOnClickListener(this);
        }

        @Override
        public void onAllowLoads() {
            super.onAllowLoads();
            IntentFilter f = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
            itemView.getContext().registerReceiver(mReceiver, f);
        }

        @Override
        public boolean onMenuItemClick(MenuItem item) {
            int itemId = item.getItemId();
            if (itemId == R.id.action_bluetooth_settings) {
                Intent intent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
                try {
                    itemView.getContext().startActivity(intent);
                } catch (Exception e) {
                    // TODO: show toast
                }
                return true;
            }
            return false;
        }

        @Override
        public void onDisallowLoads() {
            super.onDisallowLoads();
            itemView.getContext().unregisterReceiver(mReceiver);
        }

        void showOnStatus() {
            DrawableCompat.setTintColorCompat(mBluetoothDrawable, mPrimaryColor);
            mTextView.setText(R.string.enabled);
        }

        void showOffStatus() {
            DrawableCompat.setTintColorCompat(mBluetoothDrawable, mWidgetColor);
            mTextView.setText(R.string.disabled);
        }

        @Override
        void updateConfiguration() {
            if (mBluetoothAdapter == null) {
                mBluetoothDrawable.setAlpha(128);
                mTextView.setText(R.string.no_bluetooth);
            } else {
                mBluetoothDrawable.setAlpha(255);
                if (mBluetoothAdapter.isEnabled()) {
                    showOnStatus();
                } else {
                    showOffStatus();
                }
            }

        }

        @Override
        void bind(HomeItem item, int heightPx) {
            super.bind(item, heightPx);
            mChildView.setOnClickListener(this);
            updateConfiguration();
        }

        @Override
        public void onClick(View v) {
            int id = v.getId();
            if (id == R.id.overflow) {
                showOverflowMenu(v);
            } else {
                if (mBluetoothAdapter != null) {
                    if (mBluetoothAdapter.isEnabled()) {
                        mBluetoothAdapter.disable();
                    } else {
                        mBluetoothAdapter.enable();
                    }
                }
            }
        }

        private void showOverflowMenu(View v) {
            PopupMenuHelper.showPopupMenu(v, R.menu.home_item_bluetooth, this);
        }

    }

    static abstract class AbsWeatherTemperatureViewHolder extends AbsWeatherViewHolder
            implements View.OnClickListener, HomeItemConfigurationHelper.ConfigurationListener {

        @Nullable
        final ImageView mWeatherConditionView;

        final TextView mTextView;

        final TextView mTemperatureView;

        final WeatherUtils mWeatherUtils;

        public AbsWeatherTemperatureViewHolder(HomeViewWrapper view, boolean showsWallpaper) {
            super(view, showsWallpaper);
            mWeatherUtils = AppInjector.provideWeatherUtils();
            mWeatherConditionView = (ImageView) view.findViewById(R.id.weather_condition_image);
            mTextView = (TextView) view.findViewById(R.id.primary_text);
            mTemperatureView = (TextView) view.findViewById(R.id.temperature);
        }

        @Override
        void bind(HomeItem item, int heightPx) {
            mHomeItem = item;
            applySuggestedHeight(heightPx);
            mChildView.setOnClickListener(this);
            updateConfiguration();
        }

        abstract void bindWeatherData(long cellId, WeatherData weatherData, boolean isDay, String temperature);

        @Override
        void onWeatherDataReady(WeatherData weatherData) {
            long cellId = mHomeItem.mId;

            String timezone = mConfigurationHelper.getProperty(cellId, WeatherFragment.PREFERENCE_WEATHER_TIMEZONE,
                    null);

            if (timezone == null) {
                timezone = TimeZone.getDefault().getID();
            }

            boolean isDay = mWeatherUtils.isDay(timezone, weatherData);

            String unit = weatherData.unit;
            String defaultUnit = mPreferenceHelper.getDefaultWeatherTemperatureUnit();
            String displayUnit = mConfigurationHelper.getProperty(cellId, WeatherFragment.PREFERENCE_WEATHER_UNIT,
                    defaultUnit);

            String temperature = mWeatherUtils.formatTemperature(itemView.getContext(), weatherData.nowTemperature,
                    unit, displayUnit, WeatherUtils.FLAG_TEMPERATURE_NO_UNIT);

            bindWeatherData(cellId, weatherData, isDay, temperature);

        }

        @Override
        void onNoWeatherDataAvailable() {
            if (mWeatherConditionView != null) {
                mWeatherConditionView.setImageResource(R.drawable.ic_weather_unknown);
            }
            mTextView.setText(R.string.unknown);
            mTemperatureView.setText("");
        }

        @Override
        public void onClick(View v) {

            super.onClick(v);
        }

    }

    static class WeatherTemperaturePlainViewHolder extends AbsWeatherTemperatureViewHolder
            implements View.OnClickListener, HomeItemConfigurationHelper.ConfigurationListener {

        public WeatherTemperaturePlainViewHolder(HomeViewWrapper view) {
            super(view, false /* showsWallpaper */);
        }

        @Override
        void bindWeatherData(long cellId, WeatherData weatherData, boolean isDay, String temperature) {

            int iconResId = mWeatherUtils.getConditionCodeIconResId(weatherData.nowConditionCode, isDay);

            mWeatherConditionView.setImageResource(iconResId);
            setupTitle(mTextView, weatherData, cellId);
            mTemperatureView.setText(temperature);
        }
    }

    static class WeatherTemperatureWallpaperViewHolder extends AbsWeatherTemperatureViewHolder
            implements View.OnClickListener, HomeItemConfigurationHelper.ConfigurationListener {

        public WeatherTemperatureWallpaperViewHolder(HomeViewWrapper view) {
            super(view, true /* showsWallpaper */);
        }

        @Override
        protected void configurePaintJob(PaintJob.Builder paintJob) {
            paintJob.paintWithSwatch(PaintJob.SWATCH_DARK_MUTED,
                    ViewPainters.text(R.id.primary_text, R.id.temperature),
                    ViewPainters.argb(128, R.id.primary_text, R.id.temperature),
                    DrawableStartTintPainter.forIds(R.id.primary_text));
        }

        @Override
        void bindWeatherData(long cellId, WeatherData weatherData, boolean isDay, String temperature) {
            // TODO: implement properly

            int iconResId = mWeatherUtils.getConditionCodeTinyIconResId(weatherData.nowConditionCode, isDay);

            mTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(iconResId, 0, 0, 0);
            setupTitle(mTextView, weatherData, cellId);
            mTemperatureView.setText(temperature);
        }
    }

    static class AutoAdvanceHelper implements Handler.Callback {

        final SimpleArrayMap<View, AppWidgetProviderInfo> mWidgetsToAdvance = new SimpleArrayMap<>();

        private final int ADVANCE_MSG = 1;

        private final int mAdvanceInterval = 20000;

        private final int mAdvanceStagger = 250;

        private final Handler mHandler;

        boolean mVisible;

        boolean mAutoAdvanceRunning;

        private long mAutoAdvanceSentTime;

        private long mAutoAdvanceTimeLeft = -1;

        AutoAdvanceHelper() {
            mHandler = new Handler(this);
        }

        void addWidgetToAutoAdvanceIfNeeded(View hostView, AppWidgetProviderInfo appWidgetInfo) {
            if (appWidgetInfo == null || appWidgetInfo.autoAdvanceViewId == -1)
                return;
            View v = hostView.findViewById(appWidgetInfo.autoAdvanceViewId);
            if (v instanceof Advanceable) {
                mWidgetsToAdvance.put(hostView, appWidgetInfo);
                ((Advanceable) v).fyiWillBeAdvancedByHostKThx();
                updateRunning();
            }
        }

        private void updateRunning() {
            boolean autoAdvanceRunning = mVisible && !mWidgetsToAdvance.isEmpty();
            if (autoAdvanceRunning != mAutoAdvanceRunning) {
                mAutoAdvanceRunning = autoAdvanceRunning;
                if (autoAdvanceRunning) {
                    long delay = mAutoAdvanceTimeLeft == -1 ? mAdvanceInterval : mAutoAdvanceTimeLeft;
                    sendAdvanceMessage(delay);
                } else {
                    if (!mWidgetsToAdvance.isEmpty()) {
                        mAutoAdvanceTimeLeft = Math.max(0,
                                mAdvanceInterval - (System.currentTimeMillis() - mAutoAdvanceSentTime));
                    }
                    mHandler.removeMessages(ADVANCE_MSG);
                    mHandler.removeMessages(0); // Remove messages sent using postDelayed()
                }
            }
        }

        private void sendAdvanceMessage(long delay) {
            mHandler.removeMessages(ADVANCE_MSG);
            Message msg = mHandler.obtainMessage(ADVANCE_MSG);
            mHandler.sendMessageDelayed(msg, delay);
            mAutoAdvanceSentTime = System.currentTimeMillis();
        }

        void removeWidgetToAutoAdvance(View hostView) {
            if (mWidgetsToAdvance.containsKey(hostView)) {
                mWidgetsToAdvance.remove(hostView);
                updateRunning();
            }
        }

        @Override
        public boolean handleMessage(Message msg) {
            if (msg.what == ADVANCE_MSG) {
                int count = mWidgetsToAdvance.size();
                for (int i = 0; i < count; i++) {
                    View key = mWidgetsToAdvance.keyAt(i);
                    final View v = key.findViewById(mWidgetsToAdvance.get(key).autoAdvanceViewId);
                    final int delay = mAdvanceStagger * i;
                    if (v instanceof Advanceable) {
                        mHandler.postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                ((Advanceable) v).advance();
                            }
                        }, delay);
                    }
                }
                sendAdvanceMessage(mAdvanceInterval);
                return true;
            }
            return false;
        }

        public void stop() {
            mVisible = false;
            updateRunning();
        }

        public void start() {
            mVisible = true;
            updateRunning();
        }
    }

    static class DisabledAppWidgetViewHolder extends BaseViewHolder {

        final AppWidgetManager mAppWidgetManager;

        final ImageView mWidgetPreview;

        final TextView mNoWidgetTextView;

        int mAppWidgetId;

        AppWidgetUtils mAppWidgetUtils;

        public DisabledAppWidgetViewHolder(HomeViewWrapper view, AppWidgetManager appWidgetManager,
                AppWidgetUtils appWidgetUtils) {
            super(view);
            mWidgetPreview = (ImageView) view.findViewById(R.id.widget_preview);
            mNoWidgetTextView = (TextView) view.findViewById(R.id.no_widget_selected_text);
            mAppWidgetManager = appWidgetManager;
        }

        @Override
        void updateConfiguration() {
            long cellId = mHomeItem.mId;
            String appWidgetId = mConfigurationHelper.getProperty(cellId, "app_widget_id", null);
            mAppWidgetId = appWidgetId == null ? -1 : Integer.parseInt(appWidgetId);

            if (mAppWidgetId == -1) {
                mWidgetPreview.setImageResource(0);
                mNoWidgetTextView.setVisibility(View.VISIBLE);
            } else {
                mNoWidgetTextView.setVisibility(View.GONE);
                AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(mAppWidgetId);
                Bitmap image = mAppWidgetUtils.getWidgetPreviewBitmap(appWidgetInfo, null);
                mWidgetPreview.setImageBitmap(image);
            }
        }
    }

    static class AppWidgetViewHolder extends BaseViewHolder
            implements View.OnClickListener, AppsiiAppWidgetHost.HostStatusListener {

        final AppsiiAppWidgetHost mAppWidgetHost;

        final AppWidgetManager mAppWidgetManager;

        final ViewGroup mContainer;

        final AutoAdvanceHelper mAutoAdvanceHelper;

        final int mTouchSlop;

        final View mChooseWidgetButton;

        final View mErrorLoadingWidgetButton;

        final CardView mCardView;

        final int mDefaultColor;

        int mAppWidgetId;

        @Nullable
        AppWidgetHostView mAppWidgetHostView;

        AppWidgetProviderInfo mAppWidgetInfo;

        int mWidth;

        int mHeight;

        boolean mInitializedSinceLastStop;

        public AppWidgetViewHolder(HomeViewWrapper view, AppsiiAppWidgetHost appWidgetHost,
                AppWidgetManager appWidgetManager, AutoAdvanceHelper autoAdvanceHelper) {
            super(view);
            mAutoAdvanceHelper = autoAdvanceHelper;
            mAppWidgetHost = appWidgetHost;
            mAppWidgetManager = appWidgetManager;
            mChooseWidgetButton = view.findViewById(R.id.choose_app_widget_button);
            mErrorLoadingWidgetButton = view.findViewById(R.id.error_loading_widget_button);
            mContainer = (ViewGroup) view.findViewById(R.id.widget_container);
            mCardView = (CardView) view.findViewById(R.id.widget_card);

            mErrorLoadingWidgetButton.setVisibility(View.GONE);

            TypedArray a = view.getContext()
                    .obtainStyledAttributes(new int[] { R.attr.appsiAppWidgetCardBackground });
            mDefaultColor = a.getColor(0, Color.BLACK);
            a.recycle();

            mTouchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
        }

        static void updateWidgetSizeRanges(AppWidgetHostView widgetView, int w, int h) {
            Context context = widgetView.getContext();
            float density = context.getResources().getDisplayMetrics().density;
            widgetView.updateAppWidgetSize(null, (int) (w / density), (int) (h / density), (int) (w / density),
                    (int) (h / density));
        }

        public void postCreate() {
            mAppWidgetHost.registerHostStatusListener(this);
            mChooseWidgetButton.setOnClickListener(this);
            mErrorLoadingWidgetButton.setOnClickListener(this);
        }

        @Override
        public void onStartedListening() {
            updateConfiguration();
        }

        /**
         * Adds the widget to the container, returning true when successful. May fail
         * in cases where there is a RemoteException. In that case no widget is added.
         */
        private boolean addWidgetToContainer(Context context) {
            // We have a simple cache for the app-widget host-views. This
            // cache is only valid at the time where the host is listening
            // The cache is automatically cleared when the host is stopped
            // so this is just an optimization, to improve performance
            AppWidgetHostView cached = sAppWidgetViewCache.get(mAppWidgetId);
            if (cached == null) {
                mAppWidgetHostView = safelyCreateView(context, mAppWidgetId, mAppWidgetInfo);
            } else {
                mAppWidgetHostView = cached;
            }
            if (mAppWidgetHostView == null) {
                return false;
            }

            mAppWidgetHostView.setPadding(0, 0, 0, 0);
            mContainer.removeAllViews();

            // we need to remove from the parent if the view is still attached
            // to a view that is in the recycler-cache.
            ViewGroup parent = (ViewGroup) mAppWidgetHostView.getParent();
            if (parent != null) {
                parent.removeView(mAppWidgetHostView);
            }
            mContainer.addView(mAppWidgetHostView);
            if (cached == null) {
                mContainer.setAlpha(0);
                mContainer.animate().alpha(1).withEndAction(null);
            }
            return true;
        }

        private AppWidgetHostView safelyCreateView(Context context, int appWidgetId, AppWidgetProviderInfo info) {

            // This is an IPC call, and may cause a crash such as
            // java.lang.RuntimeException: system server dead?
            // Caused by: android.os.TransactionTooLargeException
            try {
                AppWidgetHostView result = mAppWidgetHost.createView(context, appWidgetId, info);
                // add it to the cache
                sAppWidgetViewCache.put(appWidgetId, result);
                return result;
            } catch (RuntimeException e) {
                return null;
            }
        }

        @Override
        public void onStoppedListening() {
            mInitializedSinceLastStop = false;
        }

        @Override
        public void onViewsCleared() {
            clearAppWidget();
        }

        void clearAppWidget() {
            mAppWidgetId = -1;
            mAppWidgetInfo = null;
            if (mAppWidgetHostView != null) {
                mAutoAdvanceHelper.removeWidgetToAutoAdvance(mAppWidgetHostView);
            }

            mContainer.animate().alpha(0).withEndAction(new Runnable() {
                @Override
                public void run() {
                    mContainer.removeAllViews();
                }
            });
            mAppWidgetHostView = null;
        }

        @Override
        void bind(HomeItem item, int heightPx) {
            super.bind(item, heightPx);
        }

        @Override
        public void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            mWidth = w;
            mHeight = h;
            if (mAppWidgetHostView != null) {
                updateWidgetSizeRanges(mAppWidgetHostView, w, h);
            }
        }

        @Override
        void updateConfiguration() {
            int old = mAppWidgetId;
            long cellId = mHomeItem.mId;

            Context context = itemView.getContext();

            String appWidgetId = mConfigurationHelper.getProperty(cellId, "app_widget_id", null);

            mAppWidgetId = appWidgetId == null ? -1 : Integer.parseInt(appWidgetId);

            boolean widgetActive;

            if (mAppWidgetId == -1) {
                mChooseWidgetButton.setVisibility(View.VISIBLE);
                widgetActive = false;
            } else if (mAppWidgetId != old) {
                mChooseWidgetButton.setVisibility(View.GONE);

                if (mAppWidgetHostView != null) {
                    mAutoAdvanceHelper.removeWidgetToAutoAdvance(mAppWidgetHostView);
                }

                mAppWidgetInfo = mAppWidgetManager.getAppWidgetInfo(mAppWidgetId);
                widgetActive = addWidgetToContainer(context);
            } else {
                mChooseWidgetButton.setVisibility(View.GONE);
                // nothing changed, so we don't need to do anything
                // we only re-create the view to ensure it is up-to-date
                widgetActive = addWidgetToContainer(context);

            }

            if (mHomeItem.mEffectColor == Color.TRANSPARENT) {
                mCardView.setCardBackgroundColor(mDefaultColor);
            } else {
                mCardView.setCardBackgroundColor(mHomeItem.mEffectColor);
            }

            if (widgetActive) {
                updateWidgetSizeRanges(mAppWidgetHostView, mWidth, mHeight);
                mAutoAdvanceHelper.addWidgetToAutoAdvanceIfNeeded(mAppWidgetHostView, mAppWidgetInfo);
            } else if (mAppWidgetId != -1) {
                mErrorLoadingWidgetButton.setVisibility(View.VISIBLE);
            }
        }

        @Override
        public void onAttached(boolean allowLoads) {
            super.onAttached(allowLoads);
            if (mAppWidgetHost.isListening() && !mInitializedSinceLastStop) {
                mInitializedSinceLastStop = true;
                updateConfiguration();
            }
            if (mAppWidgetHostView != null) {
                mAutoAdvanceHelper.addWidgetToAutoAdvanceIfNeeded(mAppWidgetHostView, mAppWidgetInfo);
            }
        }

        @Override
        public void onDetached(boolean allowLoads) {
            super.onDetached(allowLoads);

            if (mAppWidgetHostView != null) {
                mAutoAdvanceHelper.removeWidgetToAutoAdvance(mAppWidgetHostView);
            }
        }

        @Override
        public void onClick(View v) {
            int id = v.getId();
            if (id == R.id.error_loading_widget_button) {
                mErrorLoadingWidgetButton.setVisibility(View.GONE);
                updateConfiguration();
            } else {
                Context context = itemView.getContext();
                Intent intent = new Intent(context, WidgetChooserActivity.class);
                intent.putExtra(WidgetChooserActivity.EXTRA_CELL_ID, mHomeItem.mId);
                context.startActivity(intent);
            }
        }

    }

    static class PlainWeatherWindViewHolder extends AbsWeatherWindViewHolder {

        public PlainWeatherWindViewHolder(HomeViewWrapper view) {
            super(view, false /* showsWallpaper */);
        }

        @Override
        void applyWeatherData(String unit, String windSpeed, WeatherData weatherData) {
            // properly convert the speed if needed
            if (!"c".equals(unit)) {
                int speed = Math.round(mWeatherUtils.toKph(weatherData.windSpeed));
                mLargeWindmillDrawable.setWindSpeedKmh(speed);
                mSmallWindmillDrawable.setWindSpeedKmh(speed);
            } else {
                mLargeWindmillDrawable.setWindSpeedKmh(weatherData.windSpeed);
                mSmallWindmillDrawable.setWindSpeedKmh(weatherData.windSpeed);
            }

            mWindSpeedView.setText(windSpeed);
            mLargeWindmillDrawable.start();
            mSmallWindmillDrawable.start();

        }
    }

    static class WallpaperWeatherWindViewHolder extends AbsWeatherWindViewHolder {

        public WallpaperWeatherWindViewHolder(HomeViewWrapper view) {
            super(view, true /* showsWallpaper */);
        }

        @Override
        void applyWeatherData(String unit, String windSpeed, WeatherData weatherData) {
            // properly convert the speed if needed
            if (!"c".equals(unit)) {
                int speed = Math.round(mWeatherUtils.toKph(weatherData.windSpeed));
                mLargeWindmillDrawable.setWindSpeedKmh(speed);
                mSmallWindmillDrawable.setWindSpeedKmh(speed);
            } else {
                mLargeWindmillDrawable.setWindSpeedKmh(weatherData.windSpeed);
                mSmallWindmillDrawable.setWindSpeedKmh(weatherData.windSpeed);
            }

            mWindSpeedView.setText(windSpeed);
            mLargeWindmillDrawable.start();
            mSmallWindmillDrawable.start();

        }
    }

    static abstract class AbsWeatherWindViewHolder extends AbsWeatherViewHolder
            implements View.OnClickListener, HomeItemConfigurationHelper.ConfigurationListener {

        final WindmillDrawable mLargeWindmillDrawable;

        final WindmillDrawable mSmallWindmillDrawable;

        final ImageView mLargeWindmill;

        final ImageView mSmallWindmill;

        final TextView mTextView;

        final TextView mWindSpeedView;

        public AbsWeatherWindViewHolder(HomeViewWrapper view, boolean showsWallpaper) {
            super(view, showsWallpaper);
            Context context = view.getContext();
            mLargeWindmill = (ImageView) view.findViewById(R.id.weather_windmill_large);
            mSmallWindmill = (ImageView) view.findViewById(R.id.weather_windmill_medium);
            mTextView = (TextView) view.findViewById(R.id.primary_text);
            mWindSpeedView = (TextView) view.findViewById(R.id.wind_speed);

            mLargeWindmillDrawable = new WindmillDrawable(context);
            mSmallWindmillDrawable = new WindmillDrawable(context);
            mLargeWindmill.setImageDrawable(mLargeWindmillDrawable);

            mSmallWindmill.setImageDrawable(mSmallWindmillDrawable);

        }

        public void onRecycled() {
            mLargeWindmillDrawable.stop();
            mSmallWindmillDrawable.stop();
        }

        @Override
        void bind(HomeItem item, int heightPx) {
            mHomeItem = item;
            applySuggestedHeight(heightPx);
            mChildView.setOnClickListener(this);
            updateConfiguration();
        }

        @Override
        public void onAttached(boolean allowLoads) {
            super.onAttached(allowLoads);
            mLargeWindmillDrawable.start();
            mSmallWindmillDrawable.start();
        }

        @Override
        public void onDetached(boolean allowLoads) {
            super.onDetached(allowLoads);
            mLargeWindmillDrawable.stop();
            mSmallWindmillDrawable.stop();
        }

        @Override
        void onWeatherDataReady(WeatherData weatherData) {
            String defaultUnit = mPreferenceHelper.getDefaultWeatherTemperatureUnit();

            String displayUnit = mConfigurationHelper.getProperty(mHomeItem.mId,
                    WeatherFragment.PREFERENCE_WEATHER_UNIT, defaultUnit);

            long cellId = mHomeItem.mId;

            setupTitle(mTextView, weatherData, cellId);
            String unit = weatherData.unit;
            String windSpeed = mWeatherUtils.formatWindSpeed(itemView.getContext(), weatherData.windSpeed, unit,
                    displayUnit);

            applyWeatherData(unit, windSpeed, weatherData);

        }

        abstract void applyWeatherData(String unit, String windSpeed, WeatherData weatherData);

        @Override
        void onNoWeatherDataAvailable() {
            mTextView.setText(R.string.unknown);
            mWindSpeedView.setText("-");
            mLargeWindmillDrawable.stop();
            mSmallWindmillDrawable.stop();
        }

        @Override
        public void onClick(View v) {
            super.onClick(v);
        }
    }

    static class PlainWeatherSunViewHolder extends AbsWeatherSunViewHolder {

        public PlainWeatherSunViewHolder(HomeViewWrapper view) {
            super(view, false /* showsWallpaper */);
        }

        @Override
        void applyWeatherData(Context context, long cellId, int minuteNow, int sunriseMinuteOfDay,
                int sunsetMinuteOfDay, WeatherData weatherData) {
            setupTitle(mTextView, weatherData, cellId);

            // set the values on the drawable
            mSunriseDrawable.setTime(sunriseMinuteOfDay, sunsetMinuteOfDay, minuteNow);

            String time = formatAsTime(context, sunriseMinuteOfDay);
            mSunriseView.setText(time);

            time = formatAsTime(context, sunsetMinuteOfDay);
            mSunsetView.setText(time);

        }
    }

    static class WallpaperWeatherSunViewHolder extends AbsWeatherSunViewHolder {

        public WallpaperWeatherSunViewHolder(HomeViewWrapper view) {
            super(view, true /* showsWallpaper */);
            setIsRecyclable(false);
        }

        @Override
        protected void configurePaintJob(PaintJob.Builder paintJob) {
            paintJob.paintWithSwatch(PaintJob.SWATCH_DARK_VIBRANT, ViewPainters.argb(128, R.id.primary_text),
                    ViewPainters.text(R.id.primary_text, R.id.sunset, R.id.sunrise),
                    new DrawablePainter(R.id.weather_sun));
        }

        @Override
        void applyWeatherData(Context context, long cellId, int minuteNow, int sunriseMinuteOfDay,
                int sunsetMinuteOfDay, WeatherData weatherData) {
            setupTitle(mTextView, weatherData, cellId);

            // set the values on the drawable
            mSunriseDrawable.setTime(sunriseMinuteOfDay, sunsetMinuteOfDay, minuteNow);

            String time = formatAsTime(context, sunriseMinuteOfDay);
            mSunriseView.setText(time);

            time = formatAsTime(context, sunsetMinuteOfDay);
            mSunsetView.setText(time);

        }

        static class DrawablePainter extends PaintJob.BaseViewPainter {

            protected DrawablePainter(int... viewIds) {
                super(255, viewIds);
            }

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

            @Override
            protected int getTargetColorFromSwatch(Palette.Swatch swatch) {
                return swatch.getBodyTextColor();
            }

            @Override
            protected boolean applyColorsToView(View view, Palette.Swatch swatch) {
                SunriseDrawable drawable = (SunriseDrawable) ((ImageView) view).getDrawable();
                drawable.customTheme(R.drawable.ic_small_clear_day, swatch.getRgb(), swatch.getBodyTextColor(),
                        swatch.getRgb());
                return true;
            }

            @Override
            protected void applyColorToView(View view, int color) {
            }

            @Override
            protected int getCurrentColorFromView(View view) {
                return 0;
            }
        }
    }

    static abstract class AbsWeatherSunViewHolder extends AbsWeatherViewHolder
            implements View.OnClickListener, HomeItemConfigurationHelper.ConfigurationListener {

        /**
         * The string builder used by the formatter to format the time
         */
        final StringBuilder mStringBuilder = new StringBuilder();

        /**
         * The formatter used to format the time
         */
        final Formatter mFormatter = new Formatter(mStringBuilder);

        /**
         * The view containing the drawable that will draw the info
         */
        final ImageView mSunView;

        /**
         * The main text view containing the location name
         */
        final TextView mTextView;

        /**
         * The drawable doing all the work of displaying the arc and the
         * dots
         */
        final SunriseDrawable mSunriseDrawable;

        /**
         * The text-view that displays the sunrise time
         */
        final TextView mSunriseView;

        /**
         * The text-view that displays the sunset time
         */
        final TextView mSunsetView;

        final Time sTempTime = new Time();

        public AbsWeatherSunViewHolder(HomeViewWrapper view, boolean showsWallpaper) {
            super(view, showsWallpaper);
            Context context = view.getContext();
            mSunView = (ImageView) view.findViewById(R.id.weather_sun);
            mTextView = (TextView) view.findViewById(R.id.primary_text);
            mSunriseView = (TextView) view.findViewById(R.id.sunrise);
            mSunsetView = (TextView) view.findViewById(R.id.sunset);

            mSunriseDrawable = new SunriseDrawable(context);
            mSunView.setImageDrawable(mSunriseDrawable);
        }

        /**
         * Uses the formatter to render the minuteOfDay into a string.
         */
        String formatAsTime(Context context, int minuteOfDay) {
            // calculate the sunrise time in millis
            sTime.hour = minuteOfDay / 60;
            sTime.minute = minuteOfDay % 60;
            sTime.second = 0;
            long millisSunrise = sTime.normalize(true);

            // format it
            mStringBuilder.setLength(0);
            DateUtils.formatDateRange(context, mFormatter, millisSunrise, millisSunrise,
                    DateUtils.FORMAT_SHOW_TIME);

            // return the resulting string
            return mStringBuilder.toString();
        }

        @Override
        void bind(HomeItem item, int heightPx) {
            mHomeItem = item;
            applySuggestedHeight(heightPx);
            mChildView.setOnClickListener(this);
            updateConfiguration();
        }

        @Override
        void onWeatherDataReady(WeatherData weatherData) {
            long cellId = mHomeItem.mId;

            Context context = itemView.getContext();

            String timezone = mConfigurationHelper.getProperty(cellId, WeatherFragment.PREFERENCE_WEATHER_TIMEZONE,
                    null);

            // get the minutes now, sunrise and sunset minutes.
            if (timezone == null) {
                sTempTime.timezone = TimeZone.getDefault().getID();
            } else {
                sTempTime.timezone = timezone;
            }
            sTempTime.setToNow();

            int minuteNow = sTempTime.minute + 60 * sTempTime.hour;
            int sunriseMinuteOfDay = weatherData.getSunriseMinuteOfDay();
            int sunsetMinuteOfDay = weatherData.getSunsetMinuteOfDay();

            applyWeatherData(context, cellId, minuteNow, sunriseMinuteOfDay, sunsetMinuteOfDay, weatherData);
        }

        abstract void applyWeatherData(Context context, long cellId, int minuteNow, int sunriseMinuteOfDay,
                int sunsetMinuteOfDay, WeatherData weatherData);

        @Override
        void onNoWeatherDataAvailable() {
            mTextView.setText(R.string.unknown);
        }

        @Override
        public void onClick(View v) {
            super.onClick(v);
        }

    }

    /**
     * Created by nick on 24/01/15.
     */
    static class HomeSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {

        final Context mContext;

        int[] mHomeItemSpans;

        HomeSpanSizeLookup(Context context) {
            mContext = context;
        }

        void setup(List<HomeItem> homeItems) throws InconsistentRowsException {
            int row = 0;
            int totalItems = 0;

            int rowStartIdx = 0;
            long rowId = -1;

            int N = homeItems.size();
            mHomeItemSpans = new int[N];
            int pos;
            for (pos = 0; pos < N; pos++) {
                HomeItem homeItem = homeItems.get(pos);
                //if (homeItem.mPageId != pageId) continue;

                if (row != homeItem.mRowPosition) {
                    multiplyPositions(totalItems, rowStartIdx, pos);

                    totalItems = 0;
                    rowStartIdx = pos;

                    row = homeItem.mRowPosition;
                }
                mHomeItemSpans[pos] = homeItem.mColspan;
                totalItems += homeItem.mColspan;
            }
            multiplyPositions(totalItems, rowStartIdx, pos);

        }

        /**
         * Multiplies the values at the given range (a single row) with the correct multiplier.
         * This makes sure we have the proper row-spans set up.
         */
        private void multiplyPositions(int itemsInRow, int rangeStart, int rangeEnd)
                throws InconsistentRowsException {

            int multiplier;
            if (itemsInRow == 1) {
                multiplier = 12;
            } else if (itemsInRow == 2) {
                multiplier = 6;
            } else if (itemsInRow == 3) {
                multiplier = 4;
            } else if (itemsInRow == 4) {
                multiplier = 3;
            } else {
                throw new InconsistentRowsException("Invalid count in row: " + itemsInRow);
            }

            for (int i = rangeStart; i < rangeEnd; i++) {
                mHomeItemSpans[i] = mHomeItemSpans[i] * multiplier;
            }
        }

        @Override
        public int getSpanSize(int position) {

            // Something in the JB accessibility compat causes these
            // conditions so we need to check for them
            if (mHomeItemSpans == null)
                return 1;
            if (position < 0)
                return 1;
            if (position >= mHomeItemSpans.length)
                return 1;

            return mHomeItemSpans[position];
        }
    }

    static class OperationWrapper implements Operation {

        final ContentProviderOperation.Builder mBuilder;

        public OperationWrapper(ContentProviderOperation.Builder builder) {
            mBuilder = builder;
        }

        static Operation newInsert(Uri uri) {
            return new OperationWrapper(ContentProviderOperation.newInsert(uri));
        }

        static Operation newDelete(Uri uri) {
            return new OperationWrapper(ContentProviderOperation.newDelete(uri));
        }

        static Operation newUpdate(Uri uri) {
            return new OperationWrapper(ContentProviderOperation.newUpdate(uri));
        }

        @Override
        public Operation withValue(String position, int value) {
            mBuilder.withValue(position, value);
            return this;
        }

        @Override
        public Operation withValue(String position, long value) {
            mBuilder.withValue(position, value);
            return this;
        }

        @Override
        public Operation withValueBackReference(String rowId, int resultIdx) {
            mBuilder.withValueBackReference(rowId, resultIdx);
            return this;
        }

        @Override
        public Operation withSelection(String selection, String[] selectionArgs) {
            mBuilder.withSelection(selection, selectionArgs);
            return this;
        }

        @Override
        public ContentProviderOperation build() {
            return mBuilder.build();
        }
    }

    static class DefaultOpsBuilder implements OpsBuilder {

        @Override
        public Operation newInsert(Uri uri) {
            return OperationWrapper.newInsert(uri);
        }

        @Override
        public Operation newDelete(Uri uri) {
            return OperationWrapper.newDelete(uri);
        }

        @Override
        public Operation newUpdate(Uri uri) {
            return OperationWrapper.newUpdate(uri);
        }
    }

    static class InconsistentRowsException extends Exception {

        public InconsistentRowsException() {
        }

        public InconsistentRowsException(String detailMessage) {
            super(detailMessage);
        }

        public InconsistentRowsException(String detailMessage, Throwable throwable) {
            super(detailMessage, throwable);
        }

        public InconsistentRowsException(Throwable throwable) {
            super(throwable);
        }
    }

    class ProfileImageViewHolder extends BaseViewHolder
            implements View.OnClickListener, PopupMenu.OnMenuItemClickListener {

        final ImageView mImageView;

        Bitmap mUserImage;

        volatile int mViewWidth;

        volatile int mViewHeight;

        ContactBitmapLoader mImageLoader;

        public ProfileImageViewHolder(HomeViewWrapper view) {
            super(view);
            mImageView = (ImageView) view.findViewById(R.id.image);
            mOverflow.setOnClickListener(this);
        }

        Uri getProfileContactUri() {
            Context context = itemView.getContext();
            Cursor c = context.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI,
                    new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.LOOKUP_KEY, },
                    ContactsContract.Contacts.IS_USER_PROFILE + "=?", new String[] { String.valueOf("1") }, null);
            if (c == null)
                return null;

            Uri lookupUri;
            if (c.moveToNext()) {
                long id = c.getLong(0);
                String lookupKey = c.getString(1);
                Uri u = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey);

                lookupUri = Uri.withAppendedPath(u, String.valueOf(id));
            } else {
                lookupUri = null;
            }

            c.close();
            return lookupUri;
        }

        void applyUserImage(Bitmap bitmap) {
            mUserImage = bitmap;
            if (bitmap != null) {
                Log.i("Home", "Loader userImage: " + bitmap.getWidth() + "x" + bitmap.getHeight());
            }
            if (mUserImage == null) {
                mUserImage = BitmapFactory.decodeResource(mImageView.getResources(),
                        R.drawable.fallback_profile_image);
            }
            mImageView.setImageBitmap(mUserImage);

        }

        @Override
        void updateConfiguration() {
            loadUserImageIfNeeded();
        }

        @Override
        void bind(HomeItem item, int heightPx) {
            super.bind(item, heightPx);
            mChildView.setOnClickListener(this);
            if (!HomeViewWrapper.areLoadsDeferred()) {
                loadUserImageIfNeeded();
            }
        }

        private void loadUserImageIfNeeded() {
            try {
                loadUserImageIfNeededImpl();
            } catch (PermissionDeniedException e) {
                onPermissionDenied(e, Manifest.permission.READ_CONTACTS, PERMISSION_ERROR_CONTACTS_PROFILE);
            }

        }

        private void loadUserImageIfNeededImpl() throws PermissionDeniedException {
            mPermissionUtils.throwIfNotPermitted(mContext, Manifest.permission.READ_CONTACTS);
            if (mImageLoader != null) {
                mImageLoader.cancel(true);
            }
            final String contactId = mConfigurationHelper.getProperty(mHomeItem.mId, "contactId", null);
            final String lookupKey = mConfigurationHelper.getProperty(mHomeItem.mId, "lookupKey", null);

            BitmapUtils bitmapUtils = AppInjector.provideBitmapUtils();
            mImageLoader = new ContactBitmapLoader(lookupKey, contactId, bitmapUtils);
            mImageLoader.execute(itemView.getContext());
        }

        @Override
        public void onDisallowLoads() {
            super.onDisallowLoads();
            if (mImageLoader != null) {
                mImageLoader.cancel(true);
            }
        }

        @Override
        public void onAllowLoads() {
            super.onAllowLoads();
            loadUserImageIfNeeded();
        }

        @Override
        public void onAttached(boolean allowLoads) {
            super.onAttached(allowLoads);
            if (allowLoads) {
                loadUserImageIfNeeded();
            }
        }

        @Override
        public void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            mViewWidth = w;
            mViewHeight = h;
            // we wait with the load until we get the onAttached signal
            // in case the view is locked
            if (!HomeViewWrapper.areLoadsDeferred()) {
                loadUserImageIfNeeded();
            }
        }

        @Override
        public void onClick(View v) {
            int id = v.getId();
            if (id == R.id.overflow) {
                PopupMenuHelper.showPopupMenu(v, R.menu.home_item_profile_image, this);
            } else {
                // clicked on the image.
                final String contactId = mConfigurationHelper.getProperty(mHomeItem.mId, "contactId", null);
                final String lookupKey = mConfigurationHelper.getProperty(mHomeItem.mId, "lookupKey", null);

                if (contactId != null && lookupKey != null) {
                    // TODO: track this event
                    Context context = v.getContext();
                    Long cid = Long.parseLong(contactId);
                    Uri contactLookupUri = ContactsContract.Contacts.getLookupUri(cid, lookupKey);
                    Intent intent = new Intent(Intent.ACTION_VIEW, contactLookupUri);
                    AnalyticsManager analyticsManager = mAnalyticsManager;
                    analyticsManager.trackAppsiEvent(AnalyticsManager.ACTION_OPEN_HOME_ITEM,
                            AnalyticsManager.CATEGORY_PEOPLE);
                    context.startActivity(intent);

                }
            }
        }

        @Override
        public boolean onMenuItemClick(MenuItem item) {
            int itemId = item.getItemId();
            if (itemId == R.id.action_cell_image_prefs) {

                Context context = itemView.getContext();
                Intent intent = new Intent(context, CellProfileImageActivity.class);
                intent.putExtra(CellProfileImageActivity.EXTRA_CELL_ID, mHomeItem.mId);
                context.startActivity(intent);
                return true;
            }
            return false;
        }

        class ContactBitmapLoader extends AsyncTask<Context, Void, Bitmap> {

            final BitmapUtils mBitmapUtils;
            private final String mLookupKey;
            private final String mContactId;

            public ContactBitmapLoader(String lookupKey, String contactId, BitmapUtils bitmapUtils) {
                mLookupKey = lookupKey;
                mContactId = contactId;
                mBitmapUtils = bitmapUtils;
            }

            @Override
            protected Bitmap doInBackground(Context... params) {
                Context context = params[0];
                if (mLookupKey != null && mContactId != null) {
                    long id = Long.parseLong(mContactId);
                    Contact contact = RawContactsLoader.loadContact(context, id, mLookupKey);

                    if (contact != null && contact.mBitmap != null) {
                        int w = contact.mBitmap.getWidth();
                        int h = contact.mBitmap.getWidth();

                        float wscale = mViewWidth / (float) w;
                        float hscale = mViewWidth / (float) h;

                        float scale = Math.max(wscale, hscale);

                        int destH = (int) (h * scale);
                        int destW = (int) (w * scale);

                        // can happen when the view was detached earlier
                        if (destH == 0 || destW == 0)
                            return null;

                        try {
                            return Bitmap.createScaledBitmap(contact.mBitmap, destW, destH, true);
                        } catch (OutOfMemoryError e) {
                            Crashlytics.logException(e);
                            return contact.mBitmap;
                        }
                    }
                }

                Bitmap result;
                try {
                    Uri contactUri = ContactsContract.Profile.CONTENT_URI;
                    result = mBitmapUtils.decodeContactImage(contactUri, mViewWidth, mViewHeight);

                    if (result == null) {
                        Uri lookupUri = getProfileContactUri();
                        if (lookupUri != null) {
                            result = mBitmapUtils.decodeContactImage(lookupUri, mViewWidth, mViewHeight);
                        }
                    }
                } catch (PermissionDeniedException e) {
                    // this is already checked before we actually
                    // start and create the loader. So if it happens
                    // just return null.
                    return null;
                }

                return result;
            }

            @Override
            protected void onPostExecute(Bitmap bitmap) {
                applyUserImage(bitmap);
            }
        }

    }

    class SimpleTextViewViewHolder extends AbsHomeViewHolder {

        public SimpleTextViewViewHolder(HomeViewWrapper view) {
            super(view);
        }

        @Override
        void bind(HomeItem item, int heightPx) {
            applySuggestedHeight(heightPx);
            TextView myView = (TextView) mChildView;
            myView.setText("r" + item.mRowPosition + ":p" + item.mPosition + ":s" + item.mColspan + " - "
                    + item.mDisplayType);

        }
    }

    class EmptyViewViewHolder extends AbsHomeViewHolder {

        public EmptyViewViewHolder(HomeViewWrapper view) {
            super(view);
        }

        @Override
        void bind(HomeItem item, int heightPx) {
            applySuggestedHeight(heightPx);
        }
    }

    class HomeAdapterEditor {

        final ArrayList<HomeItem> mTemp = new ArrayList<>();

        /**
         * The builder used to create the operations. This allows tests to
         * override the builder and add additional checks to the created
         * operations.
         */
        @VisibleForTesting
        OpsBuilder mOpsBuilder = new DefaultOpsBuilder();

        public void moveRowDown(int rowPosition, List<ContentProviderOperation> ops) {
            if (isLastRow(rowPosition))
                return;

            boolean addedOpDown = false;
            boolean addedOpUp = false;

            int N = mHomeItems.size();
            for (int i = 0; i < N; i++) {
                HomeItem homeItem = mHomeItems.get(i);
                if (homeItem.mRowPosition == rowPosition) {
                    homeItem.mRowPosition++;
                    if (!addedOpDown) {
                        addRowPositionOp(ops, homeItem);
                        addedOpDown = true;
                    }
                } else if (homeItem.mRowPosition == rowPosition + 1) {
                    homeItem.mRowPosition--;
                    if (!addedOpUp) {
                        addRowPositionOp(ops, homeItem);
                        addedOpUp = true;
                    }
                }
            }
        }

        boolean isLastRow(int rowPosition) {
            int N = mHomeItems.size();
            for (int i = 0; i < N; i++) {
                HomeItem homeItem = mHomeItems.get(i);
                if (homeItem.mRowPosition > rowPosition)
                    return false;
            }
            return true;
        }

        private void addRowPositionOp(List<ContentProviderOperation> ops, HomeItem homeItem) {
            Uri uri = rowUri(homeItem);
            ops.add(mOpsBuilder.newUpdate(uri).withValue(HomeContract.Rows.POSITION, homeItem.mRowPosition)
                    .build());
        }

        private Uri rowUri(HomeItem homeItem) {
            return ContentUris.withAppendedId(HomeContract.Rows.CONTENT_URI, homeItem.mRowId);
        }

        public void moveRowUp(int rowPosition, List<ContentProviderOperation> ops) {
            if (rowPosition == 0)
                return;

            boolean addedOpDown = false;
            boolean addedOpUp = false;

            int N = mHomeItems.size();
            for (int i = 0; i < N; i++) {
                HomeItem homeItem = mHomeItems.get(i);
                if (homeItem.mRowPosition == rowPosition) {
                    homeItem.mRowPosition--;
                    if (!addedOpUp) {
                        addRowPositionOp(ops, homeItem);
                        addedOpUp = true;
                    }
                } else if (homeItem.mRowPosition == rowPosition - 1) {
                    homeItem.mRowPosition++;
                    if (!addedOpDown) {
                        addRowPositionOp(ops, homeItem);
                        addedOpDown = true;
                    }
                }
            }
        }

        public int getItemPosition(long id) {
            int position = 0;
            int N = mHomeItems.size();
            for (int i = 0; i < N; i++) {
                HomeItem homeItem = mHomeItems.get(i);
                if (homeItem.mId == id)
                    return position;
                position++;
            }
            return -1;
        }

        public void moveItemRight(int position, List<ContentProviderOperation> ops) {
            if (isInvalidPosition(position))
                return;
            boolean isLast = mHomeItems.size() - 1 == position;
            if (isLast)
                return;

            HomeItem item = mHomeItems.get(position);
            HomeItem next = mHomeItems.get(position + 1);
            if (next.mRowPosition != item.mRowPosition)
                return;

            item.mPosition++;
            next.mPosition--;

            ops.add(mOpsBuilder.newUpdate(homeItemUri(item)).withValue(HomeContract.Cells.POSITION, item.mPosition)
                    .build());
            ops.add(mOpsBuilder.newUpdate(homeItemUri(next)).withValue(HomeContract.Cells.POSITION, next.mPosition)
                    .build());
        }

        private boolean isInvalidPosition(int position) {
            return (position < 0 || position >= mHomeItems.size());
        }

        Uri homeItemUri(HomeItem homeItem) {
            return ContentUris.withAppendedId(HomeContract.Cells.CONTENT_URI, homeItem.mId);
        }

        boolean canMoveLeft(int position) {
            if (position == 0)
                return false;
            HomeItem item = mHomeItems.get(position);
            HomeItem prev = mHomeItems.get(position - 1);
            return prev.mRowPosition == item.mRowPosition;
        }

        boolean canMoveRight(int position) {
            if (position >= mHomeItems.size() - 1)
                return false;
            HomeItem item = mHomeItems.get(position);
            HomeItem next = mHomeItems.get(position + 1);
            return next.mRowPosition == item.mRowPosition;
        }

        public boolean canMoveUp(int position) {
            HomeItem item = mHomeItems.get(position);
            if (item.mRowPosition == 0)
                return false;
            return true;
        }

        public boolean canMoveDown(int position) {
            HomeItem item = mHomeItems.get(position);
            HomeItem last = mHomeItems.get(mHomeItems.size() - 1);
            if (item.mRowPosition == last.mRowPosition)
                return false;
            return true;
        }

        public boolean canIncreaseHeight(int position) {
            return true;
        }

        public boolean canDecreaseHeight(int position) {
            HomeItem item = mHomeItems.get(position);
            return item.mRowHeight > 1;
        }

        public boolean canIncreaseSpan(int position) {
            HomeItem homeItem = mHomeItems.get(position);

            int mRowPosition = homeItem.mRowPosition;
            int totalSpan = getTotalSpanForRow(mRowPosition);
            int itemCountInRow = getItemCountInRow(mRowPosition);

            if (itemCountInRow == 1)
                return false;
            return totalSpan < 4;
        }

        private int getTotalSpanForRow(int rowPosition) {
            int spanCount = 0;
            int N = mHomeItems.size();
            for (int i = 0; i < N; i++) {
                HomeItem homeItem = mHomeItems.get(i);
                if (homeItem.mRowPosition == rowPosition) {
                    spanCount += homeItem.mColspan;
                }
            }
            return spanCount;
        }

        private int getItemCountInRow(int rowPosition) {
            int itemCount = 0;
            int N = mHomeItems.size();
            for (int i = 0; i < N; i++) {
                HomeItem homeItem = mHomeItems.get(i);
                if (homeItem.mRowPosition == rowPosition) {
                    itemCount++;
                }
            }
            return itemCount;

        }

        public boolean canDecreaseSpan(int position) {
            HomeItem item = mHomeItems.get(position);
            if (item.mColspan == 1)
                return false;
            return item.mColspan > 1;
        }

        // walter klep 076 501 0002
        public void moveItemLeft(int position, List<ContentProviderOperation> ops) {
            boolean isFirst = position == 0;
            if (isFirst)
                return;
            if (isInvalidPosition(position))
                return;

            HomeItem item = mHomeItems.get(position);
            HomeItem prev = mHomeItems.get(position - 1);
            if (prev.mRowPosition != item.mRowPosition)
                return;

            prev.mPosition = item.mPosition;
            item.mPosition--;

            ops.add(mOpsBuilder.newUpdate(homeItemUri(item)).withValue(HomeContract.Cells.POSITION, item.mPosition)
                    .build());
            ops.add(mOpsBuilder.newUpdate(homeItemUri(prev)).withValue(HomeContract.Cells.POSITION, prev.mPosition)
                    .build());
        }

        public void increaseSpan(int position, List<ContentProviderOperation> ops) {
            HomeItem homeItem = mHomeItems.get(position);

            int mRowPosition = homeItem.mRowPosition;
            int totalSpan = getTotalSpanForRow(mRowPosition);
            int itemCountInRow = getItemCountInRow(mRowPosition);

            if (itemCountInRow == 1)
                return;
            if (totalSpan >= 4)
                return;

            if (ops != null) {
                Uri uri = homeItemUri(homeItem);
                ops.add(mOpsBuilder.newUpdate(uri).withValue(HomeContract.Cells.COLSPAN, homeItem.mColspan + 1)
                        .build());
            }
        }

        public void decreaseSpan(int position, List<ContentProviderOperation> ops) {
            HomeItem homeItem = mHomeItems.get(position);

            if (homeItem.mColspan == 1)
                return;

            int mRowPosition = homeItem.mRowPosition;
            int itemCountInRow = getItemCountInRow(mRowPosition);
            if (itemCountInRow == 1)
                return;

            if (ops != null) {
                Uri uri = homeItemUri(homeItem);
                ops.add(mOpsBuilder.newUpdate(uri).withValue(HomeContract.Cells.COLSPAN, homeItem.mColspan - 1)
                        .build());
            }

        }

        public void switchItemPositions(int positionFrom, int positionTo, List<ContentProviderOperation> ops) {
            // check for < 0
            if (positionFrom < 0)
                return;
            if (positionTo < 0)
                return;

            if (positionTo >= mHomeItems.size() || positionFrom >= mHomeItems.size())
                return;

            HomeItem h1 = mHomeItems.get(positionTo);
            HomeItem h2 = mHomeItems.get(positionFrom);

            // first, before changing anything add the ops to update the cells.
            // All we need to do is switch row-ids, col-span and position
            ops.add(mOpsBuilder.newUpdate(homeItemUri(h1)).withValue(HomeContract.Cells.COLSPAN, h2.mColspan)
                    .withValue(HomeContract.Cells._ROW_ID, h2.mRowId)
                    .withValue(HomeContract.Cells.POSITION, h2.mPosition).build());
            ops.add(mOpsBuilder.newUpdate(homeItemUri(h2)).withValue(HomeContract.Cells.COLSPAN, h1.mColspan)
                    .withValue(HomeContract.Cells._ROW_ID, h1.mRowId)
                    .withValue(HomeContract.Cells.POSITION, h1.mPosition).build());

        }

        public boolean canAddCellsInItemRow(int position) {
            if (position >= mHomeItems.size())
                return false;

            HomeItem homeItem = mHomeItems.get(position);

            int rowPosition = homeItem.mRowPosition;

            if (getItemCountInRow(rowPosition) == 4)
                return false;
            if (getTotalSpanForRow(rowPosition) == 4)
                return false;

            return true;
        }

        public void removeCellAtPosition(int position, List<ContentProviderOperation> ops) {
            if (position < 0)
                return;
            if (position >= mHomeItems.size())
                return;

            HomeItem homeItem = mHomeItems.get(position);
            int rowPosition = homeItem.mRowPosition;
            int itemsInRow = getItemCountInRow(rowPosition);

            if (itemsInRow == 1) {
                removeRowAtItemPosition(position, ops);
                return;
            }

            int size = mHomeItems.size();
            // update the next items in the same row by decreasing their position nr.
            for (int i = position + 1; i < size; i++) {
                HomeItem next = mHomeItems.get(i);
                if (next.mRowPosition == homeItem.mRowPosition && next.mPosition >= homeItem.mPosition) {

                    // add the db-op
                    ops.add(mOpsBuilder.newUpdate(homeItemUri(next))
                            .withValue(HomeContract.Cells.POSITION, next.mPosition - 1).build());
                } else {
                    break;
                }
            }

            // add the delete operations
            addDeleteCellOp(ops, homeItem);

        }

        public void removeRowAtItemPosition(int position, List<ContentProviderOperation> ops) {
            if (position >= mHomeItems.size())
                return;

            HomeItem item = mHomeItems.get(position);
            int rowPosition = item.mRowPosition;

            int size = mHomeItems.size();

            int rangeStart = -1;

            int lastRowPosition = -1;
            for (int i = size - 1; i >= 0; i--) {
                HomeItem homeItem = mHomeItems.get(i);
                if (homeItem.mRowPosition == rowPosition) {
                    addDeleteCellOp(ops, homeItem);
                } else if (homeItem.mRowPosition > rowPosition) {
                    homeItem.mRowPosition--;
                    if (lastRowPosition != homeItem.mRowPosition) {
                        addRowPositionOp(ops, homeItem);
                        lastRowPosition = homeItem.mRowPosition;
                    }
                } else if (rangeStart == -1) {
                    rangeStart = i;
                }
            }
            addDeleteRowOp(ops, item);

        }

        public void insertCellLeftOfPosition(int position, List<ContentProviderOperation> ops) {
            insertCellAtPosition(position, false, ops);
        }

        private void insertCellAtPosition(int position, boolean right, List<ContentProviderOperation> ops) {
            int rowPosition;
            int rowHeight;
            int cellPosition;
            long rowId;
            int size = mHomeItems.size();

            {
                // perform some checks that got a valid index
                if (isInvalidPosition(position))
                    return;

                HomeItem hi = mHomeItems.get(position);
                int spanCount = getTotalSpanForRow(hi.mRowPosition);
                if (spanCount > 3)
                    return;
            }

            boolean append = size <= position;
            // in case we add at the last position in the list
            if (append) {
                HomeItem item = mHomeItems.get(position - 1);
                rowId = item.mRowId;
                rowPosition = item.mRowPosition;
                cellPosition = item.mPosition + 1;
                rowHeight = item.mRowHeight;

            } else {

                HomeItem item = mHomeItems.get(position);
                rowId = item.mRowId;
                rowPosition = item.mRowPosition;
                rowHeight = item.mRowHeight;
                cellPosition = item.mPosition;
                if (right) {
                    cellPosition++;
                }

                for (int i = position; i < size; i++) {
                    // if we are adding to the right, skip the item at position
                    if (right && i == position)
                        continue;

                    // for all items in the same row, move them one to the right
                    HomeItem h = mHomeItems.get(i);
                    if (h.mRowPosition == item.mRowPosition) {
                        // add op
                        ops.add(mOpsBuilder.newUpdate(homeItemUri(h))
                                .withValue(HomeContract.Cells.POSITION, h.mPosition + 1).build());
                    }
                }
            }

            createEmptyCell(rowId, rowPosition, rowHeight, cellPosition, ops);
        }

        private HomeItem createEmptyCell(long rowId, int rowPosition, int rowHeight, int cellPosition,
                List<ContentProviderOperation> ops) {
            HomeItem toInsert = new HomeItem();
            toInsert.mDisplayType = HomeContract.Cells.DISPLAY_TYPE_UNSET;
            toInsert.mRowId = rowId;
            toInsert.mRowPosition = rowPosition;
            toInsert.mPosition = cellPosition;
            toInsert.mColspan = 1;
            toInsert.mRowHeight = rowHeight;
            toInsert.mId = nextUnsetId();

            ops.add(mOpsBuilder.newInsert(HomeContract.Cells.CONTENT_URI)
                    .withValue(HomeContract.Cells.TYPE, toInsert.mDisplayType)
                    .withValue(HomeContract.Cells._ROW_ID, toInsert.mRowId)
                    .withValue(HomeContract.Cells.POSITION, toInsert.mPosition)
                    .withValue(HomeContract.Cells.COLSPAN, toInsert.mColspan).build());

            return toInsert;
        }

        public void insertCellRightOfPosition(int position, List<ContentProviderOperation> ops) {
            insertCellAtPosition(position, true, ops);
        }

        public void insertRowAbovePosition(int position, List<ContentProviderOperation> ops) {
            if (position >= mHomeItems.size())
                return;
            if (position < 0)
                return;

            position = getFirstItemPositionOfRowAtPosition(position);

            int count = mHomeItems.size();
            int lastRowPosition = -1;

            for (int i = position; i < count; i++) {
                HomeItem homeItem = mHomeItems.get(i);
                homeItem.mRowPosition++;
                // if we haven't seen this row before create an op to update it's position
                if (lastRowPosition != homeItem.mRowPosition) {
                    addRowPositionOp(ops, homeItem);
                    lastRowPosition = homeItem.mRowPosition;
                }
            }

            HomeItem homeItem = mHomeItems.get(position);
            int rowPosition = homeItem.mRowPosition - 1;

            // we need to know the location of the insert op to pass it as a backref
            int insertOpPosition = ops.size();
            ops.add(mOpsBuilder.newInsert(HomeContract.Rows.CONTENT_URI)
                    .withValue(HomeContract.Rows.POSITION, rowPosition)
                    .withValue(HomeContract.Rows._PAGE_ID, homeItem.mPageId).withValue(HomeContract.Rows.HEIGHT, 1)
                    .build());

            // now add the op to create the cell in the column with a backref
            createEmptyCellWithRowBackRef(rowPosition, 1 /* row-height */, 0 /* cellPosition */, ops,
                    insertOpPosition);

        }

        private int getFirstItemPositionOfRowAtPosition(int position) {
            HomeItem itemAtPosition = mHomeItems.get(position);

            for (int i = position; i >= 0; i--) {
                HomeItem homeItem = mHomeItems.get(i);
                // the first item of a row with a different position is
                // the one item before the position we are looking for.
                // return position + 1;
                if (homeItem.mRowPosition != itemAtPosition.mRowPosition) {
                    return i + 1;
                }
            }
            // we are at the beginning, return 0 to indicate it should
            // be inserted as the very first item
            return 0;
        }

        private HomeItem createEmptyCellWithRowBackRef(int rowPosition, int rowHeight, int cellPosition,
                List<ContentProviderOperation> ops, int resultIdx) {
            HomeItem toInsert = new HomeItem();
            toInsert.mDisplayType = HomeContract.Cells.DISPLAY_TYPE_UNSET;
            toInsert.mRowPosition = rowPosition;
            toInsert.mPosition = cellPosition;
            toInsert.mColspan = 1;
            toInsert.mRowHeight = rowHeight;
            toInsert.mId = nextUnsetId();

            ops.add(mOpsBuilder.newInsert(HomeContract.Cells.CONTENT_URI)
                    .withValue(HomeContract.Cells.TYPE, toInsert.mDisplayType)
                    .withValueBackReference(HomeContract.Cells._ROW_ID, resultIdx)
                    .withValue(HomeContract.Cells.POSITION, toInsert.mPosition)
                    .withValue(HomeContract.Cells.COLSPAN, toInsert.mColspan).build());

            return toInsert;
        }

        public void insertRowBelowPosition(int position, List<ContentProviderOperation> ops) {
            if (isInvalidPosition(position))
                return;

            position = getLastItemPositionOfRowAtPosition(position);
            if (position >= mHomeItems.size()) {
                position = mHomeItems.size() - 1;
            }

            int lastRowPosition = -1;
            int count = mHomeItems.size();

            for (int i = position + 1; i < count; i++) {
                HomeItem homeItem = mHomeItems.get(i);
                homeItem.mRowPosition++;

                // if we haven't seen this row before create an op to update it's position
                if (lastRowPosition != homeItem.mRowPosition) {
                    addRowPositionOp(ops, homeItem);
                    lastRowPosition = homeItem.mRowPosition;
                }
            }

            HomeItem homeItem = mHomeItems.get(position);
            int rowPosition = homeItem.mRowPosition + 1;

            // we need to know the location of the insert op to pass it as a backref
            int insertOpPosition = ops.size();
            ops.add(mOpsBuilder.newInsert(HomeContract.Rows.CONTENT_URI)
                    .withValue(HomeContract.Rows.POSITION, rowPosition)
                    .withValue(HomeContract.Rows._PAGE_ID, homeItem.mPageId).withValue(HomeContract.Rows.HEIGHT, 1)
                    .build());

            // now add the op to create the cell in the column with a backref
            createEmptyCellWithRowBackRef(rowPosition, 1 /* row-height */, 0 /* cellPosition */, ops,
                    insertOpPosition);
        }

        private int getLastItemPositionOfRowAtPosition(int position) {
            int count = mHomeItems.size();
            HomeItem itemAtPosition = mHomeItems.get(position);

            for (int i = position; i < count; i++) {
                HomeItem homeItem = mHomeItems.get(i);
                // the first item of a row with a different position is
                // the one item after the position we are looking for.
                // return position + 1;
                if (homeItem.mRowPosition != itemAtPosition.mRowPosition) {
                    return i - 1;
                }
            }
            // return the last valid position in the list
            return count - 1;
        }

        public void changeCellType(int position, int displayType, List<ContentProviderOperation> ops) {
            HomeItem homeItem = mHomeItems.get(position);
            homeItem.mDisplayType = displayType;

            // Note that the deletion of the configuration is done elsewhere to
            // prevent problems with the ConfigurationHelper's internal state.
            ops.add(mOpsBuilder.newUpdate(homeItemUri(homeItem)).withValue(HomeContract.Cells.TYPE, displayType)
                    .build());

        }

        public void increaseHeight(int position, ArrayList<ContentProviderOperation> ops) {
            if (position < 0)
                return;
            if (position >= mHomeItems.size())
                return;

            HomeItem homeItem = mHomeItems.get(position);

            mTemp.clear();
            getItemsInRow(homeItem.mRowId, mTemp);

            if (ops != null) {
                Uri uri = ContentUris.withAppendedId(HomeContract.Rows.CONTENT_URI, homeItem.mRowId);

                ops.add(mOpsBuilder.newUpdate(uri).withValue(HomeContract.Rows.HEIGHT, homeItem.mRowHeight + 1)
                        .build());
            }
        }

        public void decreaseHeight(int position, ArrayList<ContentProviderOperation> ops) {
            if (position < 0)
                return;
            if (position >= mHomeItems.size())
                return;

            HomeItem homeItem = getItemAt(position);

            if (homeItem.mRowHeight <= 1)
                return;

            mTemp.clear();
            getItemsInRow(homeItem.mRowId, mTemp);

            if (ops != null) {
                Uri uri = ContentUris.withAppendedId(HomeContract.Rows.CONTENT_URI, homeItem.mRowId);

                ops.add(mOpsBuilder.newUpdate(uri).withValue(HomeContract.Rows.HEIGHT, homeItem.mRowHeight - 1)
                        .build());
            }

        }

        public HomeItem getItemAt(int position) {
            return HomeAdapter.this.getItemAt(position);
        }

        private void addDeleteCellOp(List<ContentProviderOperation> ops, HomeItem homeItem) {
            ops.add(mOpsBuilder.newDelete(homeItemUri(homeItem)).build());
        }

        private void addDeleteRowOp(List<ContentProviderOperation> ops, HomeItem homeItem) {
            ops.add(mOpsBuilder.newDelete(rowUri(homeItem)).build());
        }

        /**
         * Returns true when there are at least two rows in the adapter. Needed to see if
         * we can remove a row without removing the last one.
         */
        public boolean hasAtLeastTwoRows() {
            long rowId = -1;
            int N = mHomeItems.size();
            for (int i = 0; i < N; i++) {
                HomeItem homeItem = mHomeItems.get(i);
                if (rowId == -1) {
                    rowId = homeItem.mRowId;
                } else if (rowId != homeItem.mRowId) {
                    return true;
                }
            }
            return false;
        }

    }

}