<repo_name>Limych/Rates<file_sep>/server/Loader/MoexLoader.php
<?php
namespace Loader;

use Model;

class MoexLoader extends BaseLoader {
	
	public function load(){
		$hash = sha1(date('r'));
		$timestamp = time();
		$url = "http://www.moex.com/iss/engines/currency/markets/selt/securities.json?iss.meta=off&iss.only=securities%2Cmarketdata&securities=CETS%3AUSD000UTSTOM%2CCETS%3AEUR_RUB__TOM%2CCETS%3AEURUSD000TOM%2CCETS%3ACNYRUB_TOM%2CCETS%3AKZT000000TOM%2CCETS%3AUAH000000TOM%2CCETS%3ABYRRUB_TOM%2CCETS%3AHKDRUB_TOM%2CCETS%3AGBPRUB_TOM&lang=ru&_=$timestamp";
		
		$result = self::wget($url);
// 		if(DEBUG) var_export($result);
		
		$rates = array();
		if( $result['errno'] != 0 ){
			// error: bad url, timeout, redirect loop…
		
		}elseif( $result['http_code'] != 200 ){
			// error: no page, no permissions, no service…
		
		}else{
			$json = preg_replace(array(
					'/^[^{]+/',
					'/[^}]+$/',
			), array(
					'',
					'',
			), $result['content']);
			$json = json_decode($json, true);
		
			$ids = $values = array();
			foreach($json['securities']['data'] as $val){
				if( substr($val[2], -4) == '_TOM' ){
					$ids[$val[0]]		= $val[2];
					$values[$val[0]]	= $val[6];
				}
			}
		
			foreach($json['marketdata']['data'] as $val){
				if( isset($ids[$val[20]]) && isset($val[7]) && isset($val[8]) ){
					$rates[] = new Model\StockItem(array(
							'source'	=> 'MOEX',
							'code'		=> substr($ids[$val[20]], 0, 3),
							'faceValue'	=> $values[$val[20]],
							'currency'	=> substr($ids[$val[20]], 3, 3),
							'open'		=> $val[7],
							'last'		=> $val[8],
							'high'		=> $val[5],
							'low'		=> $val[6],
							'timestamp'	=> self::makeTimestamp($val[39]),
					));
				}
			}
			// if(DEBUG) var_export($rates);
		}
		return $rates;
	}
}
<file_sep>/server/currence_codes.ini
[Currence Codes]
AED="Дирхам ОАЭ"
AFN="Афгани"
ALL="Албанский лек"
AMD="Армянский драм"
ANG="Антильский гульден"
ARS="Аргентинское песо"
AUD="Австралийский доллар"
AZN="Азербайджанский манат"
BAM="Конвертируемая марка"
BBD="Барбадосский доллар"
BDT="Бангладешская така"
BGN="Болгарский лев"
BHD="Бахрейнский динар"
BIF="Бурундийский франк"
BND="Брунейский доллар"
BOB="Боливиано"
BRL="Бразильский реал"
BSD="Багамский доллар"
BTC="Биткойн"
BWP="Ботсванская пула"
BYR="Белорусский рубль"
BZD="Белизский доллар"
CAD="Канадский доллар"
CHF="Швейцарский франк"
CLP="Чилийское песо"
CNY="Китайский юань"
COP="Колумбийское песо"
CRC="Костариканский колон"
CUP="Кубинское песо"
CVE="Эскудо Кабо-Верде"
CZK="Чешская крона"
DJF="Франк Джибути"
DKK="Датская крона"
DOP="Доминиканское песо"
DZD="Алжирский динар"
EGP="Египетский фунт"
ETB="Эфиопский быр"
EUR="Евро"
FJD="Доллар Фиджи"
GBP="Британский фунт"
GEL="Грузинский лари"
GHS="Ганский седи"
GMD="Гамбийский даласи"
GNF="Гвинейский франк"
GTQ="Гватемальский кетсаль"
HKD="Гонконгский доллар"
HNL="Гондурасская лемпира"
HRK="Хорватская куна"
HTG="Гаитянский гурд"
HUF="Венгерский форинт"
IDR="Индонезийская рупия"
ILS="Израильский шекель"
INR="Индийская рупия"
IQD="Иракский динар"
IRR="Иранский риал"
ISK="Исландская крона"
JMD="Ямайский доллар"
JOD="Иорданский динар"
JPY="Японская иена"
KES="Кенийский шиллинг"
KHR="Камбоджийский риель"
KMF="Франк Комор"
KRW="Южнокорейская вона"
KWD="Кувейтский динар"
KYD="Доллар Каймановых островов"
KZT="Казахстанский тенге"
LAK="Лаосский кип"
LBP="Ливанский фунт"
LKR="Шри-Ланкийская рупия"
LSL="Лоти Лесото"
LTC="Лайткоин"
LTL="Литовский лит"
LYD="Ливийский динар"
MAD="Марокканский дирхам"
MDL="Молдавский лей"
MGA="Малагасийский ариари"
MKD="Македонский денар"
MMK="Мьянманский кьят"
MOP="<NAME>"
MRO="Мавританская угия"
MUR="Маврикийская рупия"
MVR="Мальдивская руфия"
MWK="Малавийская квача"
MXN="Мексиканское песо"
MYR="Малазийский ринггит"
MZN="Мозамбикский метикал"
NAD="Доллар Намибии"
NGN="Нигерийская найра"
NIO="Никарагуанская кордоба"
NOK="Норвежская крона"
NPR="Непальская рупия"
NZD="Новозеландский доллар"
OMR="Оманский риал"
PAB="Панамский бальбоа"
PEN="Перуанский новый соль"
PGK="К<NAME> — Новой Гвинеи"
PHP="Филиппинское песо"
PKR="Пакистанская рупия"
PLN="Польский злотый"
PYG="Парагвайский гуарани"
QAR="Катарский риал"
RON="Румынский лей"
RSD="Сербский динар"
RUB="Российский рубль"
RWF="Руандийский франк"
SAR="Саудовский риял"
SCR="Сейшельская рупия"
SDG="Суданский фунт"
SEK="Шведская крона"
SGD="Сингапурский доллар"
SOS="Сомалийский шиллинг"
STD="Добра Сан-Томе и Принсипи"
SVC="Сальвадорский колон"
SYP="Сирийский фунт"
SZL="Свазилендский лилангени"
THB="Тайский бат"
TND="Тунисский динар"
TRY="Турецкая лира"
TTD="Доллар Тринидада и Тобаго"
TWD="Тайваньский доллар"
TZS="Танзанийский шиллинг"
UAH="Украинская гривна"
UGX="Угандийский шиллинг"
USD="Доллар США"
UYU="Уругвайское песо"
UZS="Узбекский сум"
VEF="Венесуэльский боливар"
VND="Вьетнамский донг"
VUV="Вануатский вату"
XAF="Франк КФА BEAC"
XCD="Восточно-карибский доллар"
XOF="Франк КФА BCEAO"
XPF="Французский тихоокеанский франк"
YER="Йеменский риал"
ZAR="Южноафриканский рэнд"
ZMK="Замбийская квача"
<file_sep>/app/src/main/java/com/khrolenok/rates/controller/StockItemsAdapter.java
/*
 * Copyright (c) 2015 Andrey “Limych” Khrolenok
 *
 * 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.khrolenok.rates.controller;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.khrolenok.rates.R;
import com.khrolenok.rates.Settings;
import com.khrolenok.rates.model.StockItem;
import com.khrolenok.rates.ui.MainActivity;
import com.khrolenok.rates.ui.RatesFragment;
import com.khrolenok.rates.util.PreferencesManager;
import com.khrolenok.rates.util.StockNames;

import java.util.Date;
import java.util.List;

/**
 * Created by Limych on 17.09.2015
 */
public class StockItemsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

	private static final int TYPE_FOOTER = Integer.MIN_VALUE + 1;

	private List<StockItem> mStockItems;
	private final Context mContext;
	private final RatesFragment mFragment;

	public StockItemsAdapter(Context context, List<StockItem> stockItems, RatesFragment fragment) {
		mContext = context;
		mFragment = fragment;
		setStockItems(stockItems);
	}

	public void setStockItems(List<StockItem> stockItems) {
		this.mStockItems = stockItems;
	}

	@Override
	public int getItemCount() {
		return ( mStockItems == null ? 0 : mStockItems.size() + 1 );
	}

	@Override
	public int getItemViewType(int position) {
		if( position == getItemCount() - 1 ) return TYPE_FOOTER;
		return super.getItemViewType(position);
	}

	public StockItem getItem(int position) {
		if( position < getItemCount() - 1 ) return mStockItems.get(position);
		return null;
	}

	@Override
	public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
		final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
		switch( viewType ){
			case TYPE_FOOTER:
				return new FooterViewHolder(inflater.inflate(R.layout.fragment_rates_footer, parent, false));

			default:
				return new ViewHolder(inflater.inflate(R.layout.fragment_rates_item, parent, false));
		}
	}

	@Override
	public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
		if( getItemViewType(position) == TYPE_FOOTER ){
			final FooterViewHolder vh = (FooterViewHolder) viewHolder;
			Date updateTS = new Date(0);
			for( StockItem item : mStockItems ) {
				if( item.lastUpdate != null && item.lastUpdate.after(updateTS) )
					updateTS.setTime(item.lastUpdate.getTime());
			}
			vh.setUpdateTS(updateTS);

		} else {
			final ViewHolder vh = (ViewHolder) viewHolder;
			final StockItem stockItem = mStockItems.get(position);
			vh.setItemData(position, stockItem);
		}
	}

	public void fillValuesFromMainValue(double mainValue) {
		( (MainActivity) mContext ).mainValue = mainValue;
		for( StockItem si : mStockItems ) {
			si.value = mainValue * si.faceValue / si.lastPrice;
		}
	}

	public class ViewHolder extends RecyclerView.ViewHolder {

		private TextView symbolTV;
		private TextView nameTV;
		private TextView priceTV;
		private TextView priceChangeDirTV;
		private TextView priceChangeTV;
		private TextView valueTV;
		private ViewGroup valueContainer;

		public ViewHolder(View itemView) {
			super(itemView);

			symbolTV = (TextView) itemView.findViewById(R.id.itemSymbol);
			nameTV = (TextView) itemView.findViewById(R.id.itemName);
			priceTV = (TextView) itemView.findViewById(R.id.itemPrice);
			priceChangeDirTV = (TextView) itemView.findViewById(R.id.itemChangeDirection);
			priceChangeTV = (TextView) itemView.findViewById(R.id.itemChange);
			valueTV = (TextView) itemView.findViewById(R.id.itemValue);
			valueContainer = (ViewGroup) itemView.findViewById(R.id.itemValueContainer);
		}

		public String getStockExchangeName(Context context, String group) {
			if( group == null ){
				return "";
			}
			String stockExchangeName;
			switch( group ){
				case Settings.Rates.Groups.OFFICIAL:
					stockExchangeName = context.getString(R.string.title_official);
					break;
				case Settings.Rates.Groups.FOREX:
					stockExchangeName = context.getString(R.string.title_forex);
					break;
				case Settings.Rates.Groups.STOCK:
					stockExchangeName = context.getString(R.string.title_stock);
					break;
				default:
					stockExchangeName = group;
					break;
			}
			return "(" + stockExchangeName + ") ";
		}

		public void setItemData(final int position, final StockItem stockItem) {
			final Context context = itemView.getContext();
			final PreferencesManager prefs = PreferencesManager.getInstance();
			final boolean isLongFormat = prefs.getBoolean(PreferencesManager.PREF_LONG_FORMAT, false);


			symbolTV.setText(stockItem.symbol);
			nameTV.setText(getStockExchangeName(context, stockItem.stockExchange)
					+ StockNames.getInstance().getTitle(stockItem.symbol));
			valueTV.setText(stockItem.getValueFormatted());
			priceChangeTV.setText(stockItem.getPriceChangeFormatted(isLongFormat));

			if( stockItem.symbol.equals(stockItem.priceCurrency) ){
				priceTV.setVisibility(View.GONE);
				priceChangeDirTV.setVisibility(View.GONE);
				priceChangeTV.setVisibility(View.GONE);
			} else {
				priceTV.setVisibility(View.VISIBLE);
				priceChangeDirTV.setVisibility(View.VISIBLE);
				priceChangeTV.setVisibility(View.VISIBLE);
				priceTV.setText(stockItem.getLastPriceFormatted(isLongFormat));
				priceChangeDirTV.setText(( !stockItem.hasPriceChange()
						? R.string.change_direction_none
						: ( stockItem.getPriceChange() > 0 ? R.string.change_direction_up : R.string.change_direction_down ) ));
			}

			if( stockItem.hasPriceChange() ){
				final boolean invertColors = prefs.getBoolean(PreferencesManager.PREF_INVERT_COLORS, false);
				final int colorUp = ( !invertColors ? R.color.change_green : R.color.change_red );
				final int colorDown = ( !invertColors ? R.color.change_red : R.color.change_green );
				final int color = context.getResources().getColor(( stockItem.getPriceChange() > 0
						? colorUp : colorDown ));
				priceTV.setTextColor(color);
				priceChangeDirTV.setTextColor(color);
				priceChangeTV.setTextColor(color);
			}

			valueContainer.setOnClickListener(new View.OnClickListener() {
				@Override
				public void onClick(View v) {
					mFragment.showCalculatorDialog(position, (String) nameTV.getText(),
							stockItem.value, stockItem.symbol);
				}
			});
		}
	}

	public class FooterViewHolder extends RecyclerView.ViewHolder {

		private TextView updateTV;

		public FooterViewHolder(View itemView) {
			super(itemView);

			updateTV = (TextView) itemView.findViewById(R.id.updateTimestamp);
		}

		public void setUpdateTS(Date updateTS) {
			final Context context = itemView.getContext();
			final int updateTextFormatFlags = android.text.format.DateUtils.FORMAT_SHOW_DATE
					| android.text.format.DateUtils.FORMAT_SHOW_TIME
					| android.text.format.DateUtils.FORMAT_ABBREV_ALL;
			final String updateText = context.getString(R.string.text_updated) + " "
					+ android.text.format.DateUtils.formatDateTime(context, updateTS.getTime(),
					updateTextFormatFlags);
			updateTV.setText(updateText);
		}
	}
}
<file_sep>/app/src/main/java/com/khrolenok/rates/Settings.java
/*
 * Copyright (c) 2015 Andrey “Limych” Khrolenok
 *
 * 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.khrolenok.rates;

/**
 * Created by Limych on 07.07.2015
 */
public class Settings {
	public static final String IDEA_SUGGEST_URL = "http://exrates.reformal.ru/";

	public interface Display {
		double CHANGES_THRESHOLD = 0.1;    // Percents
	}

	public interface Rates {

		int reloadDelay = 3 * 60 * 60 * 1000;  // every 3 hours

		interface Groups {
			String OFFICIAL = "CBR";
			String STOCK = "STK";
			String FOREX = "FRX";
		}

		interface Columns {
			int GROUP = 0;
			int GOOD = 1;
			int CURRENCY = 2;
			int TIMESTAMP = 3;
			int FACE_VALUE = 4;
			int INITIAL_BID = 5;
			int LAST_BID = 6;
			int HIGH_BID = 7;
			int LOW_BID = 8;
		}
	}
}
<file_sep>/app/src/main/java/com/khrolenok/rates/ExRatesAdapter.java
//package com.khrolenok.rates;
//
//import android.content.Context;
//import android.text.Editable;
//import android.text.TextWatcher;
//import android.view.LayoutInflater;
//import android.view.View;
//import android.view.ViewGroup;
//import android.widget.BaseAdapter;
//import android.widget.EditText;
//import android.widget.ListView;
//import android.widget.TextView;
//
//import java.text.NumberFormat;
//import java.text.ParseException;
//import java.util.ArrayList;
//import java.util.List;
//
//import com.khrolenok.rates.view.controller.MainActivity;
//
///**
// * Created by Limych on 01.09.2015.
// */
//class ExRatesAdapter extends BaseAdapter {
//    private final Context mContext;
//    public List<ExRate> exRates;
//
//    public ExRatesAdapter(Context mContext) {
//        this.mContext = mContext;
//        exRates = new ArrayList<ExRate>();
//    }
//
//    public ExRatesAdapter(Context mContext, List<ExRate> exRates) {
//        this.mContext = mContext;
//        this.exRates = exRates;
//    }
//
//    public void add(ExRate exRate){
//        if( exRate != null ) {
//            exRates.add(exRate);
//        }
//    }
//
//    @Override
//    public int getCount() {
//        return exRates.size();
//    }
//
//    @Override
//    public Object getItem(int position) {
//        return exRates.get(position);
//    }
//
//    @Override
//    public long getItemId(int position) {
//        return exRates.get(position).rate_id;
//    }
//
//    public String getGroupTitle(String groupCode){
//        switch (groupCode){
//            case Settings.Rates.Groups.OFFICIAL:
//                return mContext.getString(R.string.title_official);
//            case Settings.Rates.Groups.FOREX:
//                return mContext.getString(R.string.title_forex);
//            case Settings.Rates.Groups.STOCK:
//                return mContext.getString(R.string.title_stock);
//
//            default:
//                return groupCode;
//        }
//    }
//
//    @Override
//    public View getView(final int position, View convertView, ViewGroup parent) {
//        View rowView = convertView;
//
//        if( rowView == null ) {
//            final LayoutInflater inflater = (LayoutInflater) mContext
//                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//            rowView = inflater.inflate(R.layout.activity_main_item, parent, false);
//        }
//
//        if( position % 2 == 0 ) {
//            rowView.setBackgroundColor(mContext.getResources().getColor(R.color.list_background_even));
//        }
//
//        final TextView titleView = (TextView) rowView.findViewById(R.id.item_title);
//        final TextView groupView = (TextView) rowView.findViewById(R.id.item_group);
//        final TextView rateView = (TextView) rowView.findViewById(R.id.item_price);
//        final EditText valueView = (EditText) rowView.findViewById(R.id.item_value);
//
//        final ExRate rate = exRates.get(position);
//
//        titleView.setText(rate.goodCode);
//        groupView.setText(getGroupTitle(rate.groupCode));
//        rateView.setText(rate.getLastBidFormatted());
//
//        valueView.setTag(rate);
//        final double value = ((MainActivity) mContext).mainValue * rate.faceValue / rate.lastBid;
//        valueView.setText(ExRate.format(value));
//
//        valueView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
//            @Override
//            public void onFocusChange(View v, boolean hasFocus) {
//                ((MainActivity) mContext).focus = (hasFocus ? v : null);
//            }
//        });
//
//        //every time the user adds/removes a character from the edit text, save
//        //the current value of the edit text to retrieve later
//        valueView.addTextChangedListener(new myTextWatcher(rowView));
//
//        return rowView;
//    }
//
//    private class myTextWatcher implements TextWatcher {
//        View view;
//
//        public myTextWatcher(View view) {
//            this.view = view;
//        }
//
//        @Override
//        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
//            // do nothing
//        }
//
//        @Override
//        public void onTextChanged(CharSequence s, int start, int before, int count) {
//            // do nothing
//        }
//
//        @Override
//        public void afterTextChanged(Editable s) {
//            double value = 0;
//            final double mainValue;
//            final EditText valueView = (EditText) view.findViewById(R.id.item_value);
//
//            if( valueView != ((MainActivity) mContext).focus ){
//                return;
//            }
//
//            final ExRate rate = (ExRate) valueView.getTag();
//
//            try {
//                value = NumberFormat.getInstance().parse(s.toString()).doubleValue();
//            } catch (ParseException ignored) {
//            }
//
//            if( rate.value != value ) {
//                rate.value = value;
//
//                mainValue = value * rate.lastBid / rate.faceValue;
//                ((MainActivity) mContext).setMainValue(mainValue);
//
//                ListView list = (ListView) ((MainActivity) mContext).findViewById(R.id.rates_list);
//                final int start = list.getFirstVisiblePosition();
//                for (int i = list.getLastVisiblePosition(); i >= start; i--) {
//                    final View v = list.getChildAt(i - start);
//                    if( v != view ){
//                        final EditText v_valueView = (EditText) v.findViewById(R.id.item_value);
//                        final ExRate v_rate = (ExRate) v_valueView.getTag();
//                        final double v_value = mainValue * v_rate.faceValue / v_rate.lastBid;
//                        if( v_rate.value != v_value ){
//                            v_rate.value = v_value;
//                            v_valueView.setText(ExRate.format(v_value));
//                        }
//                    }
//                }
//            }
//        }
//    }
//
//}
<file_sep>/server/Model/StockItem.php
<?php
namespace Model;

class StockItem{

	var $source;
	var $code;
	var $faceValue;
	var $currency;
	var $open;
	var $last;
	var $high;
	var $low;
	var $timestamp;
	
	function __construct($initial = null){
		if( gettype($initial) === 'array' || gettype($initial) === 'object' ){
			foreach ((array) $initial as $key => $val){
				if (!empty($key))
					$this->$key = $val;
			}
		}
	}
}
<file_sep>/app/src/main/java/com/khrolenok/rates/ExRatesGroup.java
/*
 * Copyright (c) 2015 <NAME>” Khrolenok
 *
 * 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.khrolenok.rates;

import android.content.Context;
import android.content.res.TypedArray;
import android.widget.RemoteViews;

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

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class ExRatesGroup {
    public String caption;
    public List<ExRate> exRates;

    public boolean isCompactTitleMode;
    public boolean isLongFormat;

    public ExRateViewsBounds viewsBounds;

    public ExRatesGroup(String caption) {
        this.caption = caption;
        viewsBounds = new ExRateViewsBounds();
        exRates = new ArrayList<>();
    }

    public ExRatesGroup(String caption, List<String> ratesList, JSONObject ratesJson) {
        this.caption = caption;
        viewsBounds = new ExRateViewsBounds();
        exRates = new ArrayList<>();

        for (String rate : ratesList) {
            JSONArray ratesArray;
            try {
                ratesArray = ratesJson.getJSONArray(rate);
            } catch (JSONException e) {
                continue;
            }

            add(new ExRate(ratesArray));
        }
    }

    public ExRatesGroup(String caption, String ratesGroup, List<String> ratesList, JSONObject ratesJson) {
        this.caption = caption;
        viewsBounds = new ExRateViewsBounds();
        exRates = new ArrayList<>();

        for (String rate : ratesList) {
            JSONArray ratesArray;
            try {
                ratesArray = ratesJson.getJSONArray(rate);
            } catch (JSONException e) {
                continue;
            }
            try {
                if (!ratesArray.getString(Settings.Rates.Columns.GROUP).equals(ratesGroup)) {
                    continue;
                }
            } catch (JSONException e) {
                continue;
            }

            add(new ExRate(ratesArray));
        }
    }

    public void add(ExRate exRate){
        if( exRate != null ) {
            viewsBounds.updateMaxBounds(exRate.viewsBounds);
            exRate.viewsBounds = viewsBounds;
            exRates.add(exRate);
        }
    }

    public Date getUpdateTS(){
        Date updateTS = null;
        for (ExRate exRate : exRates) {
            final Date rateTS = exRate.getUpdateTS();
            updateTS = ( updateTS == null || rateTS.after(updateTS)
                    ? rateTS
                    : updateTS );
        }
        return updateTS;
    }

    public ExRateViewsBounds calcViewsBounds(int mainFontSize, int changeFontSize){
//        if( BuildConfig.DEBUG ) Log.v(Settings.TAG, "ExRatesGroup.calcViewsBounds(" + mainFontSize + ", " + changeFontSize + ")");

        for (ExRate exRate : exRates) {
            exRate.isCompactTitleMode = isCompactTitleMode;
            exRate.isLongFormat = isLongFormat;

            exRate.calcViewsBounds(mainFontSize, changeFontSize);
            viewsBounds.updateMaxBounds(exRate, mainFontSize, changeFontSize);
        }

        return viewsBounds;
    }

    public ExRateViewsBounds calcViewsBounds(Context context){
        int mainFontSize, changeFontSize;
        final int[] textSizeAttr = new int[] { android.R.attr.textSize };
        TypedArray a = context.obtainStyledAttributes(R.style.Theme_Rates_Widget_Body, textSizeAttr);
        mainFontSize = a.getDimensionPixelSize(0, -1);
        a.recycle();
        a = context.obtainStyledAttributes(R.style.Theme_Rates_Widget_Small, textSizeAttr);
        changeFontSize = a.getDimensionPixelSize(0, -1);
        a.recycle();
        final float scaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
        if( mainFontSize <= 0 ) {
            mainFontSize = (int) (14 * scaledDensity);
        }
        if( changeFontSize <= 0 ) {
            changeFontSize = (int) (9 * scaledDensity);
        }

        return calcViewsBounds(mainFontSize, changeFontSize);
    }

    public RemoteViews buildWidgetViews(Context context, boolean isShowChange, boolean invertColors){
//        if( BuildConfig.DEBUG ) Log.v(Settings.TAG, "ExRatesGroup.buildWidgetViews(…)");

        RemoteViews ratesGroup = new RemoteViews(context.getPackageName(), R.layout.widget_layout_group);
        ratesGroup.removeAllViews(R.id.table_rates);
        ratesGroup.setTextViewText(R.id.group_caption, caption);

        for (ExRate exRate : exRates) {
            exRate.isCompactTitleMode = isCompactTitleMode;
            exRate.isLongFormat = isLongFormat;

            ratesGroup.addView(R.id.table_rates,
                    exRate.buildWidgetViews(context, isShowChange, invertColors));
        }

        return ratesGroup;
    }
}
<file_sep>/server/RestApi/HttpUtils.php
<?php
namespace RestApi;

class HttpUtils {
	
	public static function getProtocol(){
		return isset($_SERVER['SERVER_PROTOCOL'])
			? $_SERVER['SERVER_PROTOCOL']
			: 'HTTP/1.0';
	}
	
	public static function getHash($data){
		return md5(serialize($data));
	}

	public static function addHeader_ETag($data, $checkIfNoneMatch = false){
		$etag = self::getHash($data);
		
		if( $checkIfNoneMatch && isset($_SERVER['HTTP_IF_NONE_MATCH']) ){
			$noneMatch = trim($_SERVER['HTTP_IF_NONE_MATCH'], '"');
            
            if ($etag === $noneMatch) {
                header(self::getProtocol() . ' 304 Not Modified');
                return false;
            }
		}
		
		header('ETag: "' . $etag . '"');
		return true;
	}
	
	public static function addHeader_LastModified($mtime, $checkIfModifiedSince = false){
		if($checkIfModifiedSince && isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])){
			$modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
			if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
				$modifiedSince = substr($modifiedSince, 0, $semicolonPos);
			}
			
            if ($mtime === @strtotime($modifiedSince)) {
                header(self::getProtocol() . ' 304 Not Modified');
                return false;
            }
		}
		
		$lastModified = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
		header('Last-Modified: ' . $lastModified);
		return true;
	}
	
	public static function addHeader_expires($livePeriod){
		$expires = gmdate('D, d M Y H:i:s', time() + $livePeriod) . ' GMT';
		header('Expires: ' . $expires);
		header('Cache-Control: max-age=' . $livePeriod . ', public, must-revalidate');
		return true;
	}
}
<file_sep>/app/src/main/java/com/khrolenok/rates/ui/SettingsActivity.java
/*
 * Copyright (c) 2015 Andrey “Limych” Khrolenok
 *
 * 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.khrolenok.rates.ui;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceFragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;

import com.khrolenok.rates.ExRatesApplication;
import com.khrolenok.rates.R;
import com.khrolenok.rates.util.AppStore;
import com.khrolenok.rates.util.UpdateService;

/**
 * Created by Limych on 03.09.2015
 */
public class SettingsActivity extends AppCompatActivity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_settings);

		setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
		getSupportActionBar().setDisplayHomeAsUpEnabled(true);
		findViewById(R.id.tabLayout).setVisibility(View.GONE);

		if( savedInstanceState == null ){
			getFragmentManager().beginTransaction()
					.add(R.id.contentFrame, new SettingsFragment())
					.commit();
		}
	}

	public static class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {

		@Override
		public void onCreate(Bundle savedInstanceState) {
			super.onCreate(savedInstanceState);

			// Load the preferences from an XML resource
			addPreferencesFromResource(R.xml.preferences);

			if( !ExRatesApplication.isTestDevice ){
				getPreferenceScreen().removePreference(findPreference("category_debug"));

			} else {
				findPreference("device_id").setSummary(ExRatesApplication.deviceId);
				findPreference("installer").setSummary(AppStore.installer);
			}
		}

		@Override
		public void onResume() {
			super.onResume();

			// Set up a listener whenever a key changes
			getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
		}

		@Override
		public void onPause() {
			super.onPause();

			// Unregister the listener whenever a key changes
			getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
		}

		@Override
		public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
			final Context context = getActivity();

			context.sendBroadcast(new Intent(ExRatesApplication.ACTION_STOCKS_UPDATE));

			// Try to restart update service
			UpdateService.notifyUpdateNeeded(context);
		}
	}

}
<file_sep>/app/src/main/java/com/khrolenok/rates/util/ConnectionMonitor.java
/*
 * Copyright (c) 2015 Andrey “Limych” Khrolenok
 *
 * 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.khrolenok.rates.util;

import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;

import com.khrolenok.rates.BuildConfig;

import trikita.log.Log;

/**
 * Created by Limych on 18.09.2015
 */
public class ConnectionMonitor extends BroadcastReceiver {

	@Override
	public void onReceive(Context context, Intent intent) {
		final String action = intent.getAction();
		if( !action.equals(ConnectivityManager.CONNECTIVITY_ACTION) )
			return;

		boolean noConnectivity = intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
		if( !noConnectivity && isNetworkAvailable(context) ){
			if( BuildConfig.DEBUG ) Log.d("Network connection detected");

			// Restart update service
			UpdateService.notifyUpdateNeeded(context);
		}
	}

	public static void setEnabledSetting(Context context, int setting) {
		if( BuildConfig.DEBUG ) Log.d("Connection monitor status changed to " + setting);

		final ComponentName receiver = new ComponentName(context, ConnectionMonitor.class);
		final PackageManager pm = context.getPackageManager();

		pm.setComponentEnabledSetting(receiver, setting, 0);
	}

	public static boolean isNetworkAvailable(Context context) {
		final PreferencesManager prefs = PreferencesManager.getInstance();
		return isNetworkAvailable(context, prefs.getBoolean(PreferencesManager.PREF_WIFI_ONLY, false));
	}

	public static boolean isNetworkAvailable(Context context, boolean testForWiFi) {
		final ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
		final NetworkInfo activeNetwork = cm.getActiveNetworkInfo();

		final boolean isWiFi = ( activeNetwork.getType() == ConnectivityManager.TYPE_WIFI );

		if( BuildConfig.DEBUG ) Log.v("testForWiFi = " + testForWiFi + ", isWiFi = " + isWiFi);

		final boolean isNetworkAvailable = ( activeNetwork.isConnectedOrConnecting()
				&& ( !testForWiFi || isWiFi ) );

		if( BuildConfig.DEBUG ) Log.v("isNetworkAvailable = " + isNetworkAvailable);

		return isNetworkAvailable;
	}
}
<file_sep>/app/src/main/java/com/khrolenok/rates/ExRate.java
/*
 * Copyright (c) 2015 Andrey “Limych” Khrolenok
 *
 * 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.khrolenok.rates;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.widget.RemoteViews;

import com.khrolenok.rates.util.StockNames;

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

import java.text.DecimalFormat;
import java.util.Date;

/**
 * Created by Limych on 15.08.2015
 */
public class ExRate {

	public int rate_id;
	public String groupCode;
	public String goodCode;
	public String currencyCode;
	public int faceValue;
	public double initialBid;
	public double lastBid;
	public Date updateTS;
	public double value = 0;

	public boolean isCompactTitleMode;
	public boolean isLongFormat;

	public ExRateViewsBounds viewsBounds;

	public ExRate(JSONArray rateData) {
		viewsBounds = new ExRateViewsBounds();

		try{
			groupCode = rateData.getString(Settings.Rates.Columns.GROUP);
			goodCode = rateData.getString(Settings.Rates.Columns.GOOD);
			currencyCode = rateData.getString(Settings.Rates.Columns.CURRENCY);
			faceValue = rateData.getInt(Settings.Rates.Columns.FACE_VALUE);
			initialBid = rateData.getDouble(Settings.Rates.Columns.INITIAL_BID);
			lastBid = rateData.getDouble(Settings.Rates.Columns.LAST_BID);
			updateTS = new Date(rateData.getLong(Settings.Rates.Columns.TIMESTAMP) * 1000);
		} catch( JSONException ignored ){
		}

		rate_id = ( groupCode + goodCode + currencyCode ).hashCode();
	}

	public boolean isEmpty() {
		return initialBid == 0 && lastBid == 0;
	}

	public double bidChange() {
		return lastBid - initialBid;
	}

	public boolean hasBidChange() {
		return hasBidChange(Settings.Display.CHANGES_THRESHOLD);
	}

	public boolean hasBidChange(double changesThreshold) {
		return Math.abs(bidChange() * 100 / initialBid) < changesThreshold;
	}

	public String getTitle() {
		return ( isCompactTitleMode
				? StockNames.getInstance().getSymbol(goodCode)
				: goodCode );
	}

	public Date getUpdateTS() {
		return updateTS;
	}

	protected String formatValue(double value, boolean isShowSign, boolean isLongFormat) {
		int digits = ( isLongFormat ? 4 : 2 );
		double tmp = value;

		while( digits > 0 && Math.abs(tmp) > 99 ){
			tmp /= 10;
			digits--;
		}
		return String.format("%" + ( isShowSign ? "+" : "" ) + "." + digits + "f", value)
				.replace('-', '−');
	}

	public String getLastBidFormatted() {
		return formatValue(lastBid, false, isLongFormat);
	}

	public String getChangeFormatted() {
		return formatValue(bidChange(), true, isLongFormat);
	}

	protected void getTextBounds(String text, int fontSize, Rect bounds) {
		Paint p = new Paint();
		p.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL));
		p.setTextSize(fontSize);
		p.setAntiAlias(true);

		p.getTextBounds(text, 0, text.length(), bounds);
		bounds.right += bounds.left;
		bounds.left = 0;
		bounds.bottom += bounds.top;
		bounds.top = 0;
	}

	public Rect getBounds(int mainFontSize) {
		return getBounds(mainFontSize, mainFontSize);
	}

	public Rect getBounds(int mainFontSize, int changeFontSize) {
		calcViewsBounds(mainFontSize, changeFontSize);

		return new Rect(0, 0, viewsBounds.getSummaryWidth(), viewsBounds.height);
	}

	public ExRateViewsBounds calcViewsBounds(int mainFontSize, int changeFontSize) {
		Rect tb = new Rect();
		int height = 0;

		getTextBounds(getTitle(), mainFontSize, tb);
		int titleWidth = tb.width();
		height = Math.max(height, tb.height());

		getTextBounds(getLastBidFormatted(), mainFontSize, tb);
		int bidWidth = tb.width();
		height = Math.max(height, tb.height());

		getTextBounds(getChangeFormatted(), changeFontSize, tb);
		int changeWidth = tb.width();
		height = Math.max(height, tb.height());

		viewsBounds.updateMaxBounds(titleWidth, bidWidth, changeWidth, height);

		return viewsBounds;
	}

	public RemoteViews buildWidgetViews(Context context, boolean isShowChange, boolean invertColors) {
		int color = R.color.change_none;
		final int colorUp = ( !invertColors ? R.color.change_green : R.color.change_red );
		final int colorDown = ( !invertColors ? R.color.change_red : R.color.change_green );

		RemoteViews ratesItem = new RemoteViews(context.getPackageName(), R.layout.widget_layout_item);

		ratesItem.setInt(R.id.itemSymbol, "setMinWidth", viewsBounds.titleWidth);
		ratesItem.setInt(R.id.itemPrice, "setMinWidth", viewsBounds.bidWidth);

		int[] textSizeAttr = new int[] { android.R.attr.textSize };
		TypedArray a = context.obtainStyledAttributes(R.style.Theme_Rates_Widget_Body, textSizeAttr);
		final int textSize = a.getDimensionPixelSize(0, -1);
		a.recycle();
		if( textSize > 0 ){
			ratesItem.setInt(R.id.itemChangeDirection, "setMinWidth", textSize);
		}

		ratesItem.setInt(R.id.itemChange, "setMinWidth", ( isShowChange
				? viewsBounds.changeWidth
				: 0 ));

		ratesItem.setTextViewText(R.id.itemSymbol, getTitle());

		if( isEmpty() ){
			ratesItem.setTextViewText(R.id.itemPrice, context.getString(R.string.rate_none));
			ratesItem.setTextViewText(R.id.itemChangeDirection, context.getString(R.string.change_direction_none));
			ratesItem.setTextViewText(R.id.itemChange, "");
		} else {
			ratesItem.setTextViewText(R.id.itemPrice, getLastBidFormatted());
			if( hasBidChange() ){
				ratesItem.setTextViewText(R.id.itemChangeDirection, context.getString(R.string.change_direction_none));
			} else if( bidChange() > 0 ){
				ratesItem.setTextViewText(R.id.itemChangeDirection, context.getString(R.string.change_direction_up));
				color = colorUp;
			} else {
				ratesItem.setTextViewText(R.id.itemChangeDirection, context.getString(R.string.change_direction_down));
				color = colorDown;
			}
			ratesItem.setTextViewText(R.id.itemChange, ( isShowChange ? getChangeFormatted() : "" ));
		}
		ratesItem.setTextColor(R.id.itemPrice, context.getResources().getColor(color));
		ratesItem.setTextColor(R.id.itemChangeDirection, context.getResources().getColor(color));
		ratesItem.setTextColor(R.id.itemChange, context.getResources().getColor(color));

		return ratesItem;
	}

	static String format(double rate) {
		if( rate == 0 ){
			return "0";
		} else {
			final DecimalFormat df = new DecimalFormat("0.00");

			return df.format(rate);
		}
	}
}

class ExRateViewsBounds {
	public int titleWidth;
	public int bidWidth;
	public int changeWidth;

	public int height;

	public ExRateViewsBounds() {
		// Empty
	}

	public ExRateViewsBounds(int titleWidth, int bidWidth, int changeWidth, int height) {
		set(titleWidth, bidWidth, changeWidth, height);
	}

	public void set(ExRateViewsBounds viewsBounds) {
		this.titleWidth = viewsBounds.titleWidth;
		this.bidWidth = viewsBounds.bidWidth;
		this.changeWidth = viewsBounds.changeWidth;
		this.height = viewsBounds.height;
	}

	public void set(int titleWidth, int bidWidth, int changeWidth, int height) {
		this.titleWidth = titleWidth;
		this.bidWidth = bidWidth;
		this.changeWidth = changeWidth;
		this.height = height;
	}

	public void updateMaxBounds(int titleWidth, int bidWidth, int changeWidth, int height) {
		this.titleWidth = Math.max(this.titleWidth, titleWidth);
		this.bidWidth = Math.max(this.bidWidth, bidWidth);
		this.changeWidth = Math.max(this.changeWidth, changeWidth);
		this.height = Math.max(this.height, height);
	}

	public void updateMaxBounds(ExRateViewsBounds viewsBounds) {
		updateMaxBounds(viewsBounds.titleWidth, viewsBounds.bidWidth, viewsBounds.changeWidth,
				viewsBounds.height);
	}

	public void updateMaxBounds(ExRate exRate, int mainFontSize) {
		updateMaxBounds(exRate, mainFontSize, mainFontSize);
	}

	public void updateMaxBounds(ExRate exRate, int mainFontSize, int changeFontSize) {
		updateMaxBounds(exRate.calcViewsBounds(mainFontSize, changeFontSize));
	}

	public int getSummaryWidth() {
		return titleWidth + bidWidth + changeWidth;
	}
}

<file_sep>/server/index.php
<?php
use Loader;
define('DEBUG', true);	// Uncomment for debug mode

if (defined('DEBUG')){
	error_reporting(E_ALL);
	ini_set( 'display_errors', 1 );
} else {
	define('DEBUG', false);
}

$loader = include __DIR__ . '/vendor/autoload.php';
$loader->add('Loader', __DIR__ . '/');
$loader->add('Model', __DIR__ . '/');
$loader->add('RestApi', __DIR__ . '/');

// $stockItems = Loader\MoexLoader::create()->load();
$stockItems = Loader\CbrfLoader::create()->load();

RestService\Server::create('/api', new RestApi\Rates)
//     ->setDebugMode(true) //prints the debug trace, line number and file if a exception has been thrown.
	->collectRoutes()
	->run();

# http://4map.ru/service/get_rates_to_map.aspx?country=RU&latsw=55.56080032284363&lngsw=37.261019780273514&latne=55.77997579045521&lngne=37.933932377929764&zoom=12&curr1=RUB&curr2=USD&amount=1000&rate_left=-1&rate_right=-1&ww=1280&wh=616&src=1
# http://www.bmsk.ru/<file_sep>/app/src/main/java/com/khrolenok/rates/ui/RatesFragment.java
/*
 * Copyright (c) 2015 Andrey “Limych” Khrolenok
 *
 * 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.khrolenok.rates.ui;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;

import com.khrolenok.rates.ExRate;
import com.khrolenok.rates.ExRatesApplication;
import com.khrolenok.rates.ExRatesGroup;
import com.khrolenok.rates.R;
import com.khrolenok.rates.controller.StockItemsAdapter;
import com.khrolenok.rates.model.StockItem;
import com.khrolenok.rates.util.PreferencesManager;
import com.khrolenok.rates.util.UpdateService;

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

import java.util.ArrayList;
import java.util.List;

/**
 * Created by Limych on 07.09.2015
 */
public class RatesFragment extends Fragment
		implements SwipeRefreshLayout.OnRefreshListener, AppBarLayout.OnOffsetChangedListener {

	private BroadcastReceiver mBroadcastReceiver;

	private RecyclerView mRatesListView;

	private StockItemsAdapter mStockItemsAdapter;
	private SwipeRefreshLayout mQuotesRefresher;

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		final View rootView = inflater.inflate(R.layout.fragment_rates, container, false);

		mQuotesRefresher = (SwipeRefreshLayout) rootView.findViewById(R.id.srQuotesRefresher);
		mQuotesRefresher.setOnRefreshListener(this);

		mBroadcastReceiver = new BroadcastReceiver() {
			@Override
			public void onReceive(Context context, Intent intent) {
				mQuotesRefresher.setRefreshing(false);
				if( intent.getAction().equals(ExRatesApplication.ACTION_STOCKS_UPDATE) ){
					Toast.makeText(context, R.string.update_loaded, Toast.LENGTH_SHORT).show();
					populateRatesListView();

				} else if( intent.getAction().equals(ExRatesApplication.ERROR_NO_CONNECTION) ){
					Toast.makeText(context, R.string.no_internet_connection, Toast.LENGTH_SHORT).show();
				}
			}
		};

		return rootView;
	}

	@Override
	public void onOffsetChanged(AppBarLayout appBarLayout, int i) {
		mQuotesRefresher.setEnabled(( i == 0 ));
	}

	@Override
	public void onResume() {
		super.onResume();

		// Tracking the screen view
		ExRatesApplication.getInstance().trackScreenView("Rates Fragment");

		populateRatesListView();

		( (MainActivity) getActivity() ).appBarLayout.addOnOffsetChangedListener(this);

		final IntentFilter intentFilter = new IntentFilter();
		intentFilter.addAction(ExRatesApplication.ACTION_STOCKS_UPDATE);
		intentFilter.addAction(ExRatesApplication.ERROR_NO_CONNECTION);
		getActivity().registerReceiver(mBroadcastReceiver, intentFilter);
	}

	@Override
	public void onPause() {
		super.onPause();

		( (MainActivity) getActivity() ).appBarLayout.removeOnOffsetChangedListener(this);
		getActivity().unregisterReceiver(mBroadcastReceiver);
	}

	private void populateRatesListView() {
		if( mRatesListView == null ){
			mRatesListView = (RecyclerView) getActivity().findViewById(R.id.stockRatesList);
			mRatesListView.setLayoutManager(new LinearLayoutManager(getActivity()));
		}

		final PreferencesManager prefs = PreferencesManager.getInstance();
		List<String> ratesList = prefs.getStocksList();
		if( ratesList.isEmpty() ) return;

		JSONObject ratesJson;
		try{
			ratesJson = new JSONObject(prefs.getStockData());
		} catch( JSONException ignored ){
			return;
		}

		final ExRatesGroup exRatesGroup = new ExRatesGroup("", ratesList, ratesJson);

		if( mStockItemsAdapter == null ){
			mStockItemsAdapter = new StockItemsAdapter(getActivity(),
					fromExRates(exRatesGroup.exRates), this);
		} else {
			mStockItemsAdapter.setStockItems(fromExRates(exRatesGroup.exRates));
			mStockItemsAdapter.notifyDataSetChanged();
		}
		mRatesListView.setAdapter(mStockItemsAdapter);
	}

	public List<StockItem> fromExRates(List<ExRate> exRates) {
		final ArrayList<StockItem> mStockItems = new ArrayList<>();
		final double mainValue = ( (MainActivity) getActivity() ).mainValue;

		StockItem item = new StockItem();

		item.code = "RUR";
		item.symbol = "RUR";
		item.faceValue = 1;
		item.priceCurrency = "RUR";
		item.initialPrice = 1;
		item.lastPrice = 1;
		item.value = mainValue;

		mStockItems.add(item);

		for( int i = 0, cnt = exRates.size(); i < cnt; i++ ) {
			final ExRate exRate = exRates.get(i);
			item = new StockItem();

			item.code = exRate.groupCode + "_" + exRate.goodCode + "_" + exRate.currencyCode;
			item.stockExchange = exRate.groupCode;
			item.symbol = exRate.goodCode;
			item.faceValue = exRate.faceValue;
			item.priceCurrency = exRate.currencyCode;
			item.initialPrice = exRate.initialBid;
			item.lastPrice = exRate.lastBid;
			item.lastUpdate = exRate.updateTS;
			item.value = mainValue * item.faceValue / item.lastPrice;

			mStockItems.add(item);
		}

		return mStockItems;
	}

	public void showCalculatorDialog(int listItemPosition, String title, double value, String currency) {
		FragmentManager fm = getActivity().getSupportFragmentManager();

		CalculatorDialog calculatorDialog = new CalculatorDialog();
		calculatorDialog.setTargetFragment(this, 0);

		calculatorDialog.title = title;
		calculatorDialog.value = value;
		calculatorDialog.currency = currency;
		calculatorDialog.listItemPosition = listItemPosition;
		calculatorDialog.show(fm, CalculatorDialog.TAG_DIALOG);
	}

	@Override
	public void onActivityResult(int requestCode, int resultCode, Intent data) {
		super.onActivityResult(requestCode, resultCode, data);
		if( resultCode == Activity.RESULT_OK ){
			final int listItemPosition = data.getIntExtra(CalculatorDialog.TAG_LIST_ITEM_POSITION, -1);
			final double value = data.getDoubleExtra(CalculatorDialog.TAG_VALUE, 0);

			final StockItem stockItem = mStockItemsAdapter.getItem(listItemPosition);

			mStockItemsAdapter.fillValuesFromMainValue(value * stockItem.lastPrice / stockItem.faceValue);
			stockItem.value = value;
			mStockItemsAdapter.notifyDataSetChanged();
		}
	}

	@Override
	public void onRefresh() {
//		if( BuildConfig.DEBUG ){
			// Track refreshing event
			ExRatesApplication.getInstance().trackEvent("Rates", "Forced Refresh");

			UpdateService.forceUpdate(getActivity());

//		} else {
//			mQuotesRefresher.setRefreshing(false);
//			Toast.makeText(getActivity(), R.string.force_update_na, Toast.LENGTH_SHORT).show();
//		}
	}
}
<file_sep>/app/build.gradle
/*
 * Copyright (c) 2015 <NAME>” Khrolenok
 *
 * 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.
 */

apply plugin: 'com.android.application'

// read secret variables
// IMPORTANT: must be above `android {}` declaration
File secretPropsFile = file('../secret.properties')
if (secretPropsFile.exists()) {
    Properties p = new Properties()
    p.load(new FileInputStream(secretPropsFile))
    p.each { name, value ->
        safeLoad name as String, value
    }
}

android {
    compileSdkVersion 23
    buildToolsVersion '22.0.1'

    useLibrary 'org.apache.http.legacy'

    packagingOptions {
        exclude 'META-INF/LICENSE.txt'
        exclude 'META-INF/NOTICE.txt'
        exclude '.readme'
    }

    defaultConfig {
        applicationId "com.khrolenok.rates"
        minSdkVersion 16
        targetSdkVersion 23
        versionName VERSION
        versionCode getBuildVersion(1)
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }

    signingConfigs {
        release {
            storeFile safeGetFile('STORE_FILE')
            storePassword safeGet('STORE_PASSWORD')
            keyAlias safeGet('KEY_ALIAS')
            keyPassword safeGet('KEY_PASSWORD', safeGet('STORE_PASSWORD'))
            // notice how STORE_PASSWORD is used as a fallback
        }
    }

    buildTypes {
        def BOOLEAN = "boolean"
        def TRUE = "true"
        def FALSE = "false"

        all {
            buildConfigField "String", "API_ENDPOINT", project["API_ENDPOINT"]
            buildConfigField "String", "SUPPORT_EMAIL", project["SUPPORT_EMAIL"]
        }

        debug {
            buildConfigField BOOLEAN, "SHOW_ADS", TRUE
//            buildConfigField BOOLEAN, "SHOW_ADS", FALSE
//            buildConfigField BOOLEAN, "STRICT_MODE", TRUE
            buildConfigField BOOLEAN, "STRICT_MODE", FALSE
            buildConfigField BOOLEAN, "GOOGLE_ANALYTICS", FALSE
//            buildConfigField BOOLEAN, "GOOGLE_ANALYTICS", TRUE
        }

        release {
            buildConfigField BOOLEAN, "SHOW_ADS", TRUE
            buildConfigField BOOLEAN, "STRICT_MODE", FALSE
            buildConfigField BOOLEAN, "GOOGLE_ANALYTICS", TRUE

            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release as Object
            minifyEnabled true
            zipAlignEnabled true

            File releasesDir = new File(
                    safeGet('RELEASES_PARENT_DIR', '~/APKs'),
                    safeGet('FOLDER_NAME', project.group as String)
            )
            if (!releasesDir.exists())
                releasesDir.mkdirs()

            android.applicationVariants.all { variant ->
                variant.outputs.each { output ->
                    if (output.name == "release") {
                        /* base file name in a form of:
                            [package]-[versionType]-[versionName]-[versionCode]
                            ex. com.meedamian.testApp-release-1.0.0-1111
                        */
                        String fileName = [
                                defaultConfig.applicationId,
                                project.name,
                                defaultConfig.versionName,
                                getBuildVersion(android.defaultConfig.versionCode, true)
                        ].join('-')

                        //If there's no ZipAlign task it means that our artifact will be unaligned and we need to mark it as such.
                        if (!output.zipAlign)
                            fileName = fileName + '-unaligned'

                        // rename output APK
                        output.outputFile = new File(releasesDir, fileName + '.apk')

                        // copy mappings.txt (JIC)
                        if (variant.getBuildType().isMinifyEnabled()) {

                            File mappingDir = new File(releasesDir, 'mappings')
                            if (!mappingDir.exists())
                                mappingDir.mkdirs()

                            assemble << {
                                copy {
                                    from variant.mappingFile
                                    into mappingDir
                                    rename('mapping.txt', "${fileName}-mapping.txt")
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    productFlavors {
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

    compile 'com.google.android.gms:play-services-ads:7.8.0'
    compile 'com.google.android.gms:play-services-analytics:7.8.0'
    compile 'com.android.support:appcompat-v7:23.0.1'
    compile 'com.android.support:cardview-v7:23.0.1'
    compile 'com.android.support:recyclerview-v7:23.0.1'
    compile 'com.android.support:design:23.0.1'
    compile 'co.trikita:log:1.1.5'
    compile 'com.nineoldandroids:library:2.4.0' // Required for com.nhaarman.listviewanimations
    compile 'com.nhaarman.listviewanimations:lib-core:3.1.0@aar'
    compile 'com.nhaarman.listviewanimations:lib-manipulation:3.1.0@aar'
    //    compile 'com.github.navasmdc:MaterialDesign:1.5@aar'
    //    compile 'com.github.PhilJay:MPAndroidChart:v2.1.3'
    //    compile 'com.daimajia.easing:library:1.0.1@aar'
    //    compile 'com.daimajia.androidanimations:library:1.1.3@aar'
}
<file_sep>/server/Loader/BaseLoader.php
<?php
namespace Loader;

define('_COOKIE_LIFETIME', 7 * 24 * 60 * 60);
define('_HTTP_CODES_FILE', __DIR__ . '/http_codes.ini');

/**
 * @author Limych
 *
 */
abstract class BaseLoader {

	static $logFile = './loader.log';
	
	static $userAgent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.93 Safari/537.36';
	
	static $cookieFile = './cookies.cache';
	static $cookieFileLifetime = _COOKIE_LIFETIME;	// One week

	static $httpCodesFile = _HTTP_CODES_FILE;
		
	static function log($msg){
		error_log(date('Y-m-d H:i:s') . "\t$msg\n", 3, self::$logFile);
	}
	
	static function makeTimestamp($datetime, $format = 'Y-m-d H:i:s'){
		try {
			$date = \DateTime::createFromFormat($format, $datetime);
		} catch (Exception $e) {
			self::log($e->getMessage());
		}
				
		if( empty($date) ){
			$date = new \DateTime('today');
		}
	
		return $date->getTimestamp();
	}

	/**
	 * Get a web file (HTML, XHTML, XML, image, etc.) from a URL.  Return an
	 * array containing the HTTP server response header fields and content.
	 */
	static function wget( $url ){
		// Delete cookie file periodically
		if( is_file(self::$cookieFile)
				&& time() > filemtime(self::$cookieFile) + self::$cookieFileLifetime ){
			unlink(self::$cookieFile);
			self::log('Internal cookie file deleted');
		}
	
		$options = array(
				CURLOPT_CUSTOMREQUEST  => 'GET',        //set request type post or get
				CURLOPT_POST           => false,        //set to GET
				CURLOPT_USERAGENT      => self::$userAgent,	//set user agent
				CURLOPT_COOKIEFILE     => self::$cookieFile, //set cookie file
				CURLOPT_COOKIEJAR      => self::$cookieFile, //set cookie jar
				CURLOPT_RETURNTRANSFER => true,     // return web page
				CURLOPT_HEADER         => false,    // don't return headers
				CURLOPT_FOLLOWLOCATION => true,     // follow redirects
				CURLOPT_ENCODING       => '',       // handle all encodings
				CURLOPT_AUTOREFERER    => true,     // set referer on redirect
				CURLOPT_CONNECTTIMEOUT => 120,      // timeout on connect
				CURLOPT_TIMEOUT        => 120,      // timeout on response
				CURLOPT_MAXREDIRS      => 10,       // stop after 10 redirects
		);
	
		$ch      = curl_init( $url );
		curl_setopt_array( $ch, $options );
		$content = curl_exec( $ch );
		$err     = curl_errno( $ch );
		$errmsg  = curl_error( $ch );
		$result  = curl_getinfo( $ch );
		curl_close( $ch );
	
		$result['errno']   = $err;
		$result['errmsg']  = $errmsg;
		$result['content'] = $content;
	
		if( $result['errno'] != 0 ){
			// error: bad url, timeout, redirect loop…
			self::log("HTTP-Request: $url");
			self::log("Error: ({$result[errno]}) {$result[errmsg]}");
	
		}elseif( $result['http_code'] != 200 ){
			// error: no page, no permissions, no service…
			static $http_codes = null;
			if($http_codes == null){
				$http_codes = parse_ini_file(self::$httpCodesFile);
			}
			$http_msg = ( $http_codes != null ? $http_codes[$result['http_code']] : "" );
			self::log("HTTP-Request: $url");
			self::log("HTTP-Error: ({$result[http_code]}) $http_msg");
		}
	
		return $result;
	}

	/**
	 * Factory.
	 *
	 * @return BaseLoader $this
	 */
	public static function create()	{
		$clazz = get_called_class();
		return new $clazz();
	}
	
	abstract function load();
}
<file_sep>/server/Loader/CbrfLoader.php
<?php
namespace Loader;

use Model;

class CbrfLoader extends BaseLoader {

	static function _parseXmlVal($data, $timestamp){
		$valute = array();
		for ($i=0; $i < count($data); $i++) {
			$valute[$data[$i]["tag"]] = $data[$i]["value"];
		}
		if(DEBUG) var_export($valute);
		
		$price = floatval(str_replace(',', '.', str_replace('.', '', $valute['Value'])));
		return new Model\StockItem(array(
				'source'	=> 'CBRF',
				'code'		=> $valute['CharCode'],
				'faceValue'	=> $valute['Nominal'],
				'currency'	=> 'RUB',
				'open'		=> $price,
				'last'		=> $price,
				'high'		=> $price,
				'low'		=> $price,
				'timestamp'	=> $timestamp,
		));
	}
	
	static function parseXml($xml){
		$parser = xml_parser_create ('');
		xml_parser_set_option ( $parser, XML_OPTION_CASE_FOLDING, 0 );
		xml_parser_set_option ( $parser, XML_OPTION_SKIP_WHITE, 1 );
		xml_parse_into_struct ( $parser, $xml, $values, $tags );
		xml_parser_free ( $parser );

// 		if(DEBUG){
// 			var_export($values);
// 			var_export($tags);
// 			die;
// 		}
		
		$timestamp = 0;
		$stockItems = array();
		foreach ( $tags as $key => $ranges ) {
			if ($key == 'ValCurs') {
				$timestamp = self::makeTimestamp($values[$ranges[0]]['attributes']['Date'], '!d.m.Y');
// 				if(DEBUG) var_export(Date('r', $timestamp));
				
			} elseif ($key == 'Valute') {
				for($i = 0; $i < count ( $ranges ); $i += 2) {
					$offset = $ranges [$i] + 1;
					$len = $ranges [$i + 1] - $offset;
					$stockItems[] = self::_parseXmlVal( array_slice ( $values, $offset, $len ), $timestamp );
				}
			}
		}
		return $stockItems;
	}
		
	public function load(){
		$url = 'http://www.cbr.ru/scripts/XML_daily.asp';
		
		$result_last = self::wget($url);
		if(DEBUG) var_export($result_last);
		
		$rates = array();
		if( $result_last['errno'] != 0 ){
			// error: bad url, timeout, redirect loop…
		
		}elseif( $result_last['http_code'] != 200 ){
			// error: no page, no permissions, no service…
		
		}else{
			$rates = self::parseXml($result_last['content']);
			if(DEBUG) var_export($rates);
return ;
			
			$json = preg_replace(array(
					'/^[^{]+/',
					'/[^}]+$/',
			), array(
					'',
					'',
			), $result['content']);
			$json = json_decode($json, true);
		
			$ids = $values = array();
			foreach($json['securities']['data'] as $val){
				if( substr($val[2], -4) == '_TOM' ){
					$ids[$val[0]]		= $val[2];
					$values[$val[0]]	= $val[6];
				}
			}
		
			foreach($json['marketdata']['data'] as $val){
				if( isset($ids[$val[20]]) && isset($val[7]) && isset($val[8]) ){
					$rates[] = new Model\StockItem(array(
						'source'	=> 'MOEX',
						'code'		=> substr($ids[$val[20]], 0, 3),
						'faceValue'	=> $values[$val[20]],
						'currency'	=> substr($ids[$val[20]], 3, 3),
						'open'		=> $val[7],
						'last'		=> $val[8],
						'high'		=> $val[5],
						'low'		=> $val[6],
						'timestamp'	=> self::makeTimestamp($val[39]),
					));
				}
			}
			// if(DEBUG) var_export($rates);
		}
		return $rates;
	}
}
<file_sep>/server/RestApi/Rates.php
<?php
namespace RestApi;

class Rates {
	/**
	 * @return array
	 */
	public function getLast(){
		global $stockItems;
		
		$filter = 'RUB';
		
		$data = array();
		$mtime = 0;
		if(!empty($stockItems)){
			$data['fields'] = array_keys((array) $stockItems[0]);
			$data['values'] = array();
			foreach ($stockItems as $val){
				if( $val->currency === $filter ){
					$data['values'][] = array_values((array) $val);
					$mtime = max($mtime, $val->timestamp);
				}
			}
		}
		if (!HttpUtils::addHeader_LastModified($mtime, true)
// 				|| !HttpUtils::addHeader_expires(5 * 60)
				|| !HttpUtils::addHeader_ETag($data, true))
			die;
		return $data;
	}

}<file_sep>/app/src/main/java/com/khrolenok/rates/ui/WidgetProvider.java
/*
 * Copyright (c) 2015 <NAME>” Khrolenok
 *
 * 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.khrolenok.rates.ui;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.widget.RemoteViews;

import com.khrolenok.rates.BuildConfig;
import com.khrolenok.rates.ExRatesApplication;
import com.khrolenok.rates.ExRatesGroup;
import com.khrolenok.rates.R;
import com.khrolenok.rates.Settings;
import com.khrolenok.rates.util.PreferencesManager;
import com.khrolenok.rates.util.UpdateService;

import org.json.JSONObject;

import java.util.Date;
import java.util.List;

import trikita.log.Log;


/**
 * Implementation of App Widget functionality.
 */
public class WidgetProvider extends AppWidgetProvider {

	protected static boolean sIsLongFormat;

	@Override
	public void onEnabled(Context context) {
		super.onEnabled(context);

		ExRatesApplication.initApplication(context);

		// Try to start update service
		UpdateService.notifyUpdateNeeded(context);
	}

	@Override
	public void onUpdate(Context context, AppWidgetManager widgetManager, int[] widgetIds) {
		// Track widgets count
		ExRatesApplication.getInstance().trackEvent("Widget", "Count", "" + widgetIds.length);

		// There may be multiple widgets active, so update all of them
		for( int widgetId : widgetIds ) {
			final Bundle options = widgetManager.getAppWidgetOptions(widgetId);

			widgetManager.updateAppWidget(widgetId, buildLayout(context, widgetId, options));
		}
	}

	@Override
	public void onAppWidgetOptionsChanged(Context context, AppWidgetManager widgetManager,
	                                      int widgetId, Bundle newOptions) {
		// Instruct the widget manager to update the widget
		widgetManager.updateAppWidget(widgetId,
				buildLayout(context, widgetId, newOptions));

		super.onAppWidgetOptionsChanged(context, widgetManager, widgetId, newOptions);
	}

	@Override
	public void onReceive(@NonNull Context context, @NonNull Intent intent) {
		if( intent.getAction().equals(ExRatesApplication.ACTION_STOCKS_UPDATE) ){
			final AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
			final int[] widgetIds = widgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class));

			onUpdate(context, widgetManager, widgetIds);
			return;
		}

		super.onReceive(context, intent);
	}

	/**
	 * Returns number of cells needed for given size of the widget.
	 *
	 * @param size Widget size in dp.
	 * @return Size in number of cells.
	 */
	protected static int getCellsForSize(int size) {
		int n = 2;
		while( 70 * n - 30 < size ){
			++n;
		}
		return n - 1;
	}

	/**
	 * Returns layout for widget.
	 *
	 * @param context  Application mContext
	 * @param widgetId Widget ID
	 * @param options  Widget options
	 * @return Widget layout
	 */
	public static RemoteViews buildLayout(Context context, int widgetId, Bundle options) {
		if( BuildConfig.DEBUG ) Log.v("Rebuilding widget #" + widgetId + " layout");

		final int widgetMinWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH);
		final int widgetMinHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT);
		final int wCells = getCellsForSize(widgetMinWidth);
		final int hCells = getCellsForSize(widgetMinHeight);
		final int widgetLayout = ( wCells > hCells ? R.layout.widget_layout_h
				: R.layout.widget_layout_v );
		final boolean isUseCompactView = ( widgetLayout == R.layout.widget_layout_h
				? ( wCells <= 3 ) : ( wCells <= 1 ) );
		final boolean isShowChange = ( widgetLayout == R.layout.widget_layout_v && wCells >= 2 );

		final PreferencesManager prefs = PreferencesManager.getInstance();
		final boolean prefsLong = prefs.getBoolean(PreferencesManager.PREF_LONG_FORMAT, false);
		sIsLongFormat = prefsLong
				&& ( widgetLayout == R.layout.widget_layout_h && wCells >= 4
				|| widgetLayout == R.layout.widget_layout_v && wCells >= 2 );

		// Track widget settings
		ExRatesApplication.getInstance().trackEvent("Widget", "Size", "Width_" + wCells);
		ExRatesApplication.getInstance().trackEvent("Widget", "Size", "Height_" + hCells);
		ExRatesApplication.getInstance().trackEvent("Widget", "Size", wCells + "×" + hCells);
		ExRatesApplication.getInstance().trackEvent("Widget", "NumFormat", (prefsLong ? "Long" : "Short"));

		List<String> ratesList = prefs.getStocksList();
		if( ratesList.isEmpty() ){
			return new RemoteViews(context.getPackageName(), R.layout.widget_layout_loading);
		}

		JSONObject ratesJson;
		try{
			ratesJson = new JSONObject(prefs.getStockData());
		} catch( Exception ignored ){
			return new RemoteViews(context.getPackageName(), R.layout.widget_layout_loading);
		}

		RemoteViews widget = new RemoteViews(context.getPackageName(), widgetLayout);
		widget.removeAllViews(R.id.rates);

		Date updateTS = new Date(0);

		widget.addView(R.id.rates,
				buildRatesGroup(context, context.getString(R.string.title_official) + ":", ratesJson,
						ratesList, Settings.Rates.Groups.OFFICIAL, isUseCompactView, isShowChange,
						updateTS));
		if( wCells >= 2 || hCells >= 2 ){
			widget.addView(R.id.rates,
					buildRatesGroup(context, context.getString(R.string.title_stock) + ":", ratesJson,
							ratesList, Settings.Rates.Groups.STOCK, isUseCompactView, isShowChange,
							updateTS));
		}
		if( wCells >= 3 || hCells >= 2 ){
			widget.addView(R.id.rates,
					buildRatesGroup(context, context.getString(R.string.title_forex) + ":", ratesJson,
							ratesList, Settings.Rates.Groups.FOREX, isUseCompactView, isShowChange,
							updateTS));
		}

		// Show updating timestamp
		final String updateTextPrefix = ( wCells >= 2 ? context.getString(R.string.text_updated) : "" );
		final int updateTextFormatFlags = android.text.format.DateUtils.FORMAT_SHOW_DATE
				| android.text.format.DateUtils.FORMAT_SHOW_TIME
				| android.text.format.DateUtils.FORMAT_ABBREV_ALL;
		final String updateText = updateTextPrefix + " "
				+ android.text.format.DateUtils.formatDateTime(context, updateTS.getTime(),
				updateTextFormatFlags);
		widget.setTextViewText(R.id.updateTimestamp, updateText);

		Intent appIntent = new Intent(context, MainActivity.class);
		PendingIntent appPendingIntent = PendingIntent.getActivity(context, 0, appIntent, 0);
		widget.setOnClickPendingIntent(R.id.widget, appPendingIntent);

		return widget;
	}

	protected static RemoteViews buildRatesGroup(Context context, String caption,
	                                             JSONObject ratesJson, List<String> ratesList, String ratesGroup,
	                                             boolean isUseCompactView, boolean isShowChange, Date updateTS) {
		if( isUseCompactView ){
			isShowChange = false;
		}

		ExRatesGroup exRatesGroup = new ExRatesGroup(caption, ratesGroup, ratesList, ratesJson);

		exRatesGroup.isCompactTitleMode = isUseCompactView;
		exRatesGroup.isLongFormat = sIsLongFormat;

		exRatesGroup.calcViewsBounds(context);

		final Date groupTS = exRatesGroup.getUpdateTS();
		if( groupTS.after(updateTS) ){
			updateTS.setTime(groupTS.getTime());
		}

		final PreferencesManager prefs = PreferencesManager.getInstance();
		final boolean invertColors = prefs.getBoolean(PreferencesManager.PREF_INVERT_COLORS, false);

		return exRatesGroup.buildWidgetViews(context, isShowChange, invertColors);
	}
}

<file_sep>/server/exchange_rates.php
<?php
define('BASENAME',		preg_replace('/(.+)\..*$/', '$1', basename(__FILE__)));

define('CACHE_FILE',	BASENAME . '.hist');
define('CACHE_LIFE',	5 * 60);	// 5 min

define('INTERNAL_COOKIE_FILE',	'cookies.txt');
define('INTERNAL_COOKIE_LIFE',	10000);

define('HISTORY_FILE',	BASENAME . '.history');

define('HTTP_CODES_FILE',	dirname(BASENAME) . '/http_codes.ini');

define('LOG_FILE',	BASENAME . '.log');



header('Content-Type: text/plain; charset=UTF-8');

if( isset($_REQUEST['debug']) ){
	define('DEBUG', true);
	error_reporting(E_ALL);
	ini_set('display_errors', true);

	// print json_encode($exrates->getLastRates());
	
	die();
}
define('DEBUG', false);

if( file_exists(CACHE_FILE) ){
	$exrates = unserialize(file_get_contents(CACHE_FILE));
}else{
	$exrates = new ExRates();
}
if( !file_exists(CACHE_FILE) || ( (filemtime(CACHE_FILE) + CACHE_LIFE) < time() ) ){
	update_rates();
	// if(DEBUG) app_log(var_export($rates, true));
	
	file_put_contents(CACHE_FILE, serialize($exrates));
	app_log('Rates updated');
}

$timestamp = filemtime(CACHE_FILE);
$tsstring = gmdate('D, d M Y H:i:s ', $timestamp) . 'GMT';
$etag = md5($timestamp);

$if_modified_since = !isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? false : $_SERVER['HTTP_IF_MODIFIED_SINCE'];
$if_none_match = !isset($_SERVER['HTTP_IF_NONE_MATCH']) ? false : $_SERVER['HTTP_IF_NONE_MATCH'];
if ( ( !$if_none_match || ($if_none_match && $if_none_match == $etag) ) &&
		($if_modified_since && $if_modified_since == $tsstring) ) {
    header('HTTP/1.1 304 Not Modified');
    exit;
} else {
    header("Last-Modified: $tsstring");
    header("ETag: \"$etag\"");

	print json_encode($exrates->getLastRates());
}



class ExRates {
	public $rates;
	
	function __construct(){
		$this->rates = array();
	}
	
	function setRate($timestamp, $group, $currency, $good, $face_value, $bid_open, $bid_last, $bid_high = -1, $bid_low = -1){
		$group = strtoupper($group);
		$currency = strtoupper($currency);
		$good = strtoupper($good);
		$date = date('Y_z', $timestamp);
		
		if( !is_array($this->rates) ){
			$this->rates = array();
		}
		if( !isset($this->rates[$group]) || !is_array($this->rates[$group]) ){
			$this->rates[$group] = array();
			ksort($this->rates);
		}
		if( !isset($this->rates[$group][$good]) || !is_array($this->rates[$group][$good]) ){
			$this->rates[$group][$good] = array();
			ksort($this->rates[$group]);
		}
		if( !isset($this->rates[$group][$good][$currency]) || !is_array($this->rates[$group][$good][$currency]) ){
			$this->rates[$group][$good][$currency] = array();
			ksort($this->rates[$group][$good]);
		}
		if( !isset($this->rates[$group][$good][$currency][$date])
				|| !is_array($this->rates[$group][$good][$currency][$date])
				|| $this->rates[$group][$good][$currency][$date][3] < $timestamp ){
					
			$this->rates[$group][$good][$currency][''] = $date;
			$this->rates[$group][$good][$currency][$date] = array(
				$group,			// 0
				$good,			// 1
				$currency,		// 2
				$timestamp,		// 3
				$face_value,	// 4
				$bid_open,		// 5
				$bid_last,		// 6
				$bid_high,		// 7
				$bid_low,		// 8
			);
		}
	}

	function getLastRates(){
		$last_rates = array();
		foreach($this->rates as $group_key => $groups){
			foreach($groups as $good_key => $goods){
				foreach($goods as $currency_key => $curr){
					$last_rates[$group_key.'_'.$good_key.'_'.$currency_key] = $curr[$curr['']];
				}
			}
		}
		return $last_rates;
	}
}



function make_timestamp($datetime, $format = 'Y-m-d H:i:s'){
	try {
		$date = DateTime::createFromFormat($format, $datetime);
	} catch (Exception $e) {
		$msg = $e->getMessage();
		app_log($msg);
		if(DEBUG) var_export($msg);
	}
	
	if( empty($date) ){
		$date = new DateTime('today');
	}
	
	return $date->getTimestamp();
}



function app_log($msg){
	if(DEBUG){
		$msg = "DEBUG\t" . $msg;
	}
	error_log(date('Y-m-d H:i:s') . "\t$msg\n", 3, LOG_FILE);
}



function fix_rates($old_rates){
	$values = array();
	$rates = $old_rates;
	foreach( $old_rates as $key => $val ){
		$from = substr($key, 0, 3);
		$new_key = substr($key, 3, 3) . $from . substr($key, 6);
		if( $from == "RUB" && !isset($rates[$new_key]) ){
			$key = $new_key;
			$val[3] = 1 / $val[3];
			$val[4] = 1 / $val[4];
			$rates[$key] = $val;
		}
	}

	foreach( $rates as $key => $val ){
		$key = substr($key, 0, 3);
		if( $values[$key] < $val[2] )
			$values[$key] = $val[2];
	}
	foreach( $rates as $key => $val ){
		$from = substr($key, 0, 3);
		if( isset($values[$from]) && ($values[$from] != $val[2]) ){
			$val[3] = $val[3] * $values[$from] / $val[2];
			$val[4] = $val[4] * $values[$from] / $val[2];
			$val[2] = $values[$from];
			$rates[$key] = $val;
		}
	}
	return $rates;
}



function update_rates(){
	global $exrates;
	
	$update = array();
	$update = array_merge($update, get_moex_rates());
	$update = array_merge($update, get_cbr_rates());
	$update = array_merge($update, get_forex_rates());
	$update = array_merge($update, get_commodities_rates());

	$update = fix_rates($update);

	// app_log(var_export($update, true));
	foreach($update as $key => $val){
		$exrates->setRate($val[1], $val[0], $val[7], $val[8], $val[2], $val[3], $val[4], $val[5], $val[6]);
	}
}



/**
 * Get a web file (HTML, XHTML, XML, image, etc.) from a URL.  Return an
 * array containing the HTTP server response header fields and content.
 */
function get_web_page( $url ){
	$user_agent='Mozilla/5.0 (Windows NT 6.1; rv:8.0) Gecko/20100101 Firefox/8.0';

	// Delete cookie file periodically
	if( rand(0, INTERNAL_COOKIE_LIFE) === 0 ){
		unlink(INTERNAL_COOKIE_FILE);
		app_log('Internal cookie file deleted');
	}
	
	$options = array(
		CURLOPT_CUSTOMREQUEST  => 'GET',        //set request type post or get
		CURLOPT_POST           => false,        //set to GET
		CURLOPT_USERAGENT      => $user_agent,	//set user agent
		CURLOPT_COOKIEFILE     => INTERNAL_COOKIE_FILE, //set cookie file
		CURLOPT_COOKIEJAR      => INTERNAL_COOKIE_FILE, //set cookie jar
		CURLOPT_RETURNTRANSFER => true,     // return web page
		CURLOPT_HEADER         => false,    // don't return headers
		CURLOPT_FOLLOWLOCATION => true,     // follow redirects
		CURLOPT_ENCODING       => '',       // handle all encodings
		CURLOPT_AUTOREFERER    => true,     // set referer on redirect
		CURLOPT_CONNECTTIMEOUT => 120,      // timeout on connect
		CURLOPT_TIMEOUT        => 120,      // timeout on response
		CURLOPT_MAXREDIRS      => 10,       // stop after 10 redirects
	);
	
	$ch      = curl_init( $url );
	curl_setopt_array( $ch, $options );
	$content = curl_exec( $ch );
	$err     = curl_errno( $ch );
	$errmsg  = curl_error( $ch );
	$result  = curl_getinfo( $ch );
	curl_close( $ch );

	$result['errno']   = $err;
	$result['errmsg']  = $errmsg;
	$result['content'] = $content;
	
	// app_log("HTTP-Request: $url");
	if( $result['errno'] != 0 ){
		// error: bad url, timeout, redirect loop…
		app_log("HTTP-Request: $url");
		app_log("Error: ({$result[errno]}) {$result[errmsg]}");

	}elseif( $result['http_code'] != 200 ){
		// error: no page, no permissions, no service…
		static $http_codes = null;
		if($http_codes == null){
			$http_codes = parse_ini_file(HTTP_CODES_FILE);
		}
		$http_msg = $http_codes[$result['http_code']];
		app_log("HTTP-Request: $url");
		app_log("HTTP-Error: ({$result[http_code]}) $http_msg");
	}

	return $result;
}



function get_moex_rates(){
	$hash = sha1(date('r'));
	$timestamp = time();
	$url = "http://www.moex.com/iss/engines/currency/markets/selt/securities.json?iss.meta=off&iss.only=securities%2Cmarketdata&securities=CETS%3AUSD000UTSTOM%2CCETS%3AEUR_RUB__TOM%2CCETS%3AEURUSD000TOM%2CCETS%3ACNYRUB_TOM%2CCETS%3AKZT000000TOM%2CCETS%3AUAH000000TOM%2CCETS%3ABYRRUB_TOM%2CCETS%3AHKDRUB_TOM%2CCETS%3AGBPRUB_TOM&lang=ru&_=$timestamp";

	$result = get_web_page($url);
	// if(DEBUG) var_export($result);

	$rates = array();
	if( $result['errno'] != 0 ){
		// error: bad url, timeout, redirect loop…

	}elseif( $result['http_code'] != 200 ){
		// error: no page, no permissions, no service…

	}else{
		$json = preg_replace(array(
				'/^[^{]+/',
				'/[^}]+$/',
			), array(
				'',
				'',
			), $result['content']);
		$json = json_decode($json, true);
		// if(DEBUG) var_export($json);

		$ids = $values = array();
		foreach($json['securities']['data'] as $val){
			if( substr($val[2], -4) == '_TOM' ){
				$ids[$val[0]]		= $val[2];
				$values[$val[0]]	= $val[6];
			}
		}
		// if(DEBUG) var_export($ids);

		foreach($json['marketdata']['data'] as $val){
			if( isset($ids[$val[20]]) && isset($val[7]) && isset($val[8]) ){
				$rates[$ids[$val[20]]] = array(	// Cross name
					"STK",				// 0 - Cross type
					make_timestamp($val[39]),			// 1 - Timestamp
					$values[$val[20]],	// 2 - Face value
					$val[7],			// 3 - Bid on open
					$val[8],			// 4 - Last bid
					$val[5],			// 5 - High bid
					$val[6],			// 6 - Low bid
					substr($ids[$val[20]], 3, 3),	// 7 - Cross curr
					substr($ids[$val[20]], 0, 3),	// 8 - Cross good
				);
			}
		}
		// if(DEBUG) var_export($rates);
	}
	return $rates;
}



function cbr_parse_table($content){
	preg_match('|<h2>Центральный банк Российской Федерации установил с ([\d\.]+)|uSis', $content, $m);
	// if(DEBUG) var_export($m[0]);
	// Convert dd.mm.yyyy to yyyy-mm-dd
	$timestamp = make_timestamp($m[1], 'd.m.Y');

	preg_match('|<table class="data">.+?</table>|uSis', $content, $m);
	// if(DEBUG) var_export($m[0]);

	preg_match_all('|<tr><td>.+?</td>\s*<td>(.+?)</td>\s*<td>(.+?)</td>\s*<td>.+?</td>\s*<td>(.+?)</td>\s*|uSis', $m[0], $matches, PREG_SET_ORDER);
	// if(DEBUG) var_export($matches);
	
	$rates = array();
	foreach($matches as $m){
		$rates[$m[1] . 'RUB_CBR'] = array(	// Cross name
			"CBR",								// Cross type
			$timestamp,							// Timestamp
			intval($m[2]),						// Face value
			floatval(strtr($m[3], ',', '.')),	// Bid
		);
	}
	// if(DEBUG) var_export($rates);
	// if(DEBUG) app_log(var_export($rates, true));

	return $rates;
}



// Альтернатива: http://www.cbr.ru/scripts/XML_daily.asp?date_req=07.09.2015
// и для последней даты http://www.cbr.ru/scripts/XML_daily.asp
function get_cbr_rates(){
	$url = "http://www.cbr.ru/currency_base/daily.aspx?date_req=";

	$result_open = get_web_page($url . date("d.m.Y"));
	$result_last = get_web_page($url . date("d.m.Y", time() + 86400));
	// if(DEBUG) var_export($result_open);
	// if(DEBUG) var_export($result_last);
	
	$rates = array();
	if( $result_open['errno'] != 0 ){
		// error: bad url, timeout, redirect loop…

	}elseif( $result_open['http_code'] != 200 ){
		// error: no page, no permissions, no service…

	}else{
		$rates = cbr_parse_table($result_open['content']);
		// if(DEBUG) var_export($rates);
		
		$tmp = cbr_parse_table($result_last['content']);
		foreach($tmp as $key => $val){
			// If new CBR rates not set, leave last rates unchanged
			if( $rates[$key][1] == $val[1] ){
				app_log('Tomorrow CBR rates for ' . $key . ' currently are not set.');
				unset($rates[$key]);
			}
			
			$rates[$key][4] = $val[3];							// Last bid
			$rates[$key][5] = max($val[3], $rates[$key][3]);	// High bid
			$rates[$key][6] = min($val[3], $rates[$key][3]);	// Low bid
			$rates[$key][7] = substr($key, 3, 3);				// 7 - Cross curr
			$rates[$key][8] = substr($key, 0, 3);				// 8 - Cross good
		}
		// if(DEBUG) var_export($rates);
		// if(DEBUG) app_log(var_export($rates, true));
	}
	return $rates;
}



// Альтернатива: http://partners.instaforex.com/ru/quotes_description.php/
function get_forex_rates(){
	$url = "http://fxrates.ru.forexprostools.com/index_single_crosses.php?currency=79&bid=hide&ask=hide&last=show&change=hide&change_in_percents=hide&last_update=show";

	$result = get_web_page($url);
	// if(DEBUG) var_export($result);

	$rates = array();
	if( $result['errno'] != 0 ){
		// error: bad url, timeout, redirect loop…

	}elseif( $result['http_code'] != 200 ){
		// error: no page, no permissions, no service…

	}else{
		preg_match('|<table id="cross_rate_1".+?</table>|uSis', $result['content'], $m);
		// if(DEBUG) var_export($m[0]);

		preg_match_all('|<span class="ftqa11bb arial_11_b">([^<]+)</span></td></nobr><td class="[^>]+>([^<]+)</td><td class="[^>]+>([^<]+)</td><td class="[^>]+>([^<]+)</td><td class="[^>]+>([^<]+)</td><td class="[^>]+>([^<]+)</td></tr>|uSis', $m[0], $matches, PREG_SET_ORDER);
		// if(DEBUG) var_export($matches);
		// if(DEBUG) app_log(var_export($matches, true));
		
		foreach($matches as $m){
			$rates[str_replace('/', '', $m[1]) . '_FRX'] = array(	// Cross name
				"FRX",												// 0 — Cross type
				make_timestamp($m[6], 'H:i:s'),						// 1 — Timestamp
				1,													// 2 — Face value
				floatval($m[3]),									// 3 — Bid on open
				floatval($m[2]),									// 4 — Last bid
				floatval($m[4]),									// 5 — High bid
				floatval($m[5]),									// 6 — Low bid
				substr($m[1], 4, 3),								// 7 — Cross curr
				substr($m[1], 0, 3),								// 8 — Cross good
			);
		}
		// if(DEBUG) var_export($rates);
	}
	return $rates;
}



// Альтернатива: https://www.quandl.com/resources/commodity-data#Commodity-Dashboards
function get_commodities_rates(){
	$url = "http://comrates.investing.com/index.php?force_lang=1&pairs_ids=8830;8831;8833;8836;8910;&open=show&month=hide&change=hide&change_in_percents=hide&last_update=show";

	$result = get_web_page($url);
	// if(DEBUG) var_export($result);

	$rates = array();
	if( $result['errno'] != 0 ){
		// error: bad url, timeout, redirect loop…

	}elseif( $result['http_code'] != 200 ){
		// error: no page, no permissions, no service…

	}else{
		preg_match('|<table id="cross_rate_1".+?</table>|uSis', $result['content'], $m);
		// if(DEBUG) var_export($m[0]);

		preg_match_all('|<span class="ftqa11bb arial_11_b">([^<]+)</span></td></nobr><td class="[^>]+>([^<]+)</td><td class="[^>]+>([^<]+)</td><td class="[^>]+>([^<]+)</td><td class="[^>]+>([^<]+)</td><td class="[^>]+>([^<]+)</td></tr>|uSis', $m[0], $matches, PREG_SET_ORDER);
		// if(DEBUG) var_export($matches);
		// if(DEBUG) app_log(var_export($matches, true));
		
		static $codes = array(
			'Gold'		=> 'GCZ15',
			'Silver'	=> 'SIQ15',
			'Copper'	=> 'HGQ15',
			'Platinum'	=> 'PLQ15',
			'Brent&nbsp;Oil'	=> 'LCOV5',
		);
		
		foreach($matches as $m){
			$rates[$codes[$m[1]] . '_CMM'] = array(	// Cross name
				"CMM",							// 0 — Cross type
				make_timestamp($m[6], 'H:i:s'),	// 1 — Timestamp
				1,								// 2 — Face value
				floatval($m[3]),				// 3 — Bid on open
				floatval($m[2]),				// 4 — Last bid
				floatval($m[4]),				// 5 — High bid
				floatval($m[5]),				// 6 — Low bid
				'USD',							// 7 — Cross curr
				$codes[$m[1]],					// 8 — Cross good
			);
		}
		// if(DEBUG) var_export($rates);
	}
	return $rates;
}
<file_sep>/build.gradle
/*
 * Copyright (c) 2015 Andrey “Limych” Khrolenok
 *
 * 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.
 */

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.3.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
        maven { url "https://jitpack.io" }
    }
}

// get variables && prevent crashing if missing
String safeGet(String name, String defaultValue = '') {
    hasProperty(name) ? project[name] : defaultValue
}

File safeGetFile(String name) {
    String fileName = safeGet(name, null)
    fileName != null ? file(fileName) : null
}

// loads variables from a file to `project` so they can be `safeGet`-ed later
def safeLoad(String name, Object value, Boolean override = false) {
    if (!hasProperty(name) || override)
        project.set name, value
}

Integer getBuildVersion(defaultVersion, Boolean increment = false) {
    File verFile = file('version.properties')

    if (!verFile.canRead())
        verFile.createNewFile()

    Properties props = new Properties()

    props.load new FileInputStream(verFile)
    String currentCodeVersion = props['VERSION_CODE']

    if (currentCodeVersion == null)
        currentCodeVersion = defaultVersion ?: 1

    if (increment) {
        Integer bumpedCodeVersion = currentCodeVersion.toInteger() + 1

        props['VERSION_CODE'] = bumpedCodeVersion.toString()

        props.store verFile.newWriter(), "Build version updated with each release build"
        currentCodeVersion = bumpedCodeVersion
    }

    currentCodeVersion as Integer
}
<file_sep>/app/src/main/java/com/khrolenok/rates/util/PreferencesManager.java
/*
 * Copyright (c) 2015 <NAME>” Khrolenok
 *
 * 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.khrolenok.rates.util;

import android.content.Context;

import java.util.ArrayList;

public class PreferencesManager {

	// Public preferences
	public static final String PREF_STOCKS_LIST = "stocksList";
	public static final String PREF_INVERT_COLORS = "invert_colors";
	public static final String PREF_LONG_FORMAT = "long_format";
	public static final String PREF_WIFI_ONLY = "wifi_only";

	// Private preferences
	public static final String PREF_STOCKS_DATA = "_stocksData";
	public static final String PREF_UPDATE_TIME = "_updateTime";

	private static PreferencesManager prefsManager;
	private TinyDB tinyDB;


	public static synchronized PreferencesManager getInstance() {
		if( prefsManager == null ){
			prefsManager = new PreferencesManager();
		}
		return prefsManager;
	}

	public void init(Context context) {
		tinyDB = new TinyDB(context);
	}

	public boolean contains(String key) {
		return tinyDB.contains(key);
	}

	public void addStockSymbol(String stockSymbol) {
		ArrayList<String> stocksList = tinyDB.getListString(PREF_STOCKS_LIST);
		stocksList.add(stockSymbol);
		tinyDB.putListString(PREF_STOCKS_LIST, stocksList);
	}

	public void removeStockSymbol(String stockSymbol) {
		ArrayList<String> stocksList = tinyDB.getListString(PREF_STOCKS_LIST);
		stocksList.remove(stockSymbol);
		tinyDB.putListString(PREF_STOCKS_LIST, stocksList);
	}

	public boolean stocksListContains(String stockSymbol) {
		ArrayList<String> stocksList = tinyDB.getListString(PREF_STOCKS_LIST);
		return stocksList.contains(stockSymbol);
	}

	public ArrayList<String> getStocksList() {
		return tinyDB.getListString(PREF_STOCKS_LIST);
	}

	public void setStocksList(ArrayList<String> stocksList) {
		tinyDB.putListString(PREF_STOCKS_LIST, stocksList);
	}

	public void setBoolean(String key, boolean value) {
		tinyDB.putBoolean(key, value);
	}

	public boolean getBoolean(String key, boolean defaultValue) {
		return tinyDB.getBoolean(key, defaultValue);
	}

	public void setStockData(String stocksData) {
		tinyDB.putString(PREF_STOCKS_DATA, stocksData);
	}

	public String getStockData() {
		return tinyDB.getString(PREF_STOCKS_DATA);
	}

	public void setUpdateTime(long value) {
		tinyDB.putLong(PREF_UPDATE_TIME, value);
	}

	public long getUpdateTime() {
		return tinyDB.getLong(PREF_UPDATE_TIME, 0);
	}
}
