com.codebutler.farebot.transit.SuicaTransitData.java Source code

Java tutorial

Introduction

Here is the source code for com.codebutler.farebot.transit.SuicaTransitData.java

Source

/*
 * SuicaTransitData.java
 *
 * Authors:
 * Eric Butler <eric@codebutler.com>
 *
 * Based on code from http://code.google.com/p/nfc-felica/
 * nfc-felica by Kazzz. See project URL for complete author information.
 *
 * 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.
 *
 * Thanks to these resources for providing additional information about the Suica format:
 * http://www.denno.net/SFCardFan/
 * http://jennychan.web.fc2.com/format/suica.html
 * http://d.hatena.ne.jp/baroqueworksdev/20110206/1297001722
 * http://handasse.blogspot.com/2008/04/python-pasorisuica.html
 * http://sourceforge.jp/projects/felicalib/wiki/suica
 *
 * Some of these resources have been translated into English at:
 * https://github.com/codebutler/farebot/wiki/Suica
 */

package com.codebutler.farebot.transit;

import android.app.Application;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Parcel;
import android.util.Log;
import com.codebutler.farebot.FareBotApplication;
import com.codebutler.farebot.ListItem;
import com.codebutler.farebot.R;
import com.codebutler.farebot.card.felica.FelicaBlock;
import com.codebutler.farebot.card.felica.FelicaCard;
import com.codebutler.farebot.card.felica.FelicaService;
import net.kazzz.felica.lib.FeliCaLib;
import net.kazzz.felica.lib.Util;
import org.apache.commons.lang.ArrayUtils;

import java.text.NumberFormat;
import java.util.*;

import static com.codebutler.farebot.card.felica.DBUtil.*;

public class SuicaTransitData extends TransitData {
    private SuicaTrip[] mTrips;

    public Creator<SuicaTransitData> CREATOR = new Creator<SuicaTransitData>() {
        public SuicaTransitData createFromParcel(Parcel parcel) {
            return new SuicaTransitData(parcel);
        }

        public SuicaTransitData[] newArray(int size) {
            return new SuicaTransitData[size];
        }
    };

    public static boolean check(FelicaCard card) {
        return (card.getSystem(FeliCaLib.SYSTEMCODE_SUICA) != null);
    }

    public static TransitIdentity parseTransitIdentity(FelicaCard card) {
        return new TransitIdentity("Suica", null); // FIXME: Could be ICOCA, etc.
    }

    public SuicaTransitData(Parcel parcel) {
        mTrips = new SuicaTrip[parcel.readInt()];
        parcel.readTypedArray(mTrips, SuicaTrip.CREATOR);
    }

    public SuicaTransitData(FelicaCard card) {
        FelicaService service = card.getSystem(FeliCaLib.SYSTEMCODE_SUICA)
                .getService(FeliCaLib.SERVICE_SUICA_HISTORY);

        long previousBalance = -1;

        List<SuicaTrip> trips = new ArrayList<SuicaTrip>();

        // Read blocks oldest-to-newest to calculate fare.
        FelicaBlock[] blocks = service.getBlocks();
        for (int i = (blocks.length - 1); i >= 0; i--) {
            FelicaBlock block = blocks[i];

            SuicaTrip trip = new SuicaTrip(block, previousBalance);
            previousBalance = trip.getBalance();

            if (trip.getTimestamp() == 0) {
                continue;
            }

            trips.add(trip);
        }

        // Return trips in descending order.
        Collections.reverse(trips);

        mTrips = trips.toArray(new SuicaTrip[trips.size()]);
    }

    @Override
    public String getBalanceString() {
        if (mTrips.length > 0)
            return mTrips[0].getBalanceString();
        return null;
    }

    @Override
    public String getSerialNumber() {
        // FIXME: Find where this is on the card.
        return null;
    }

    @Override
    public Trip[] getTrips() {
        return mTrips;
    }

    @Override
    public Refill[] getRefills() {
        return null;
    }

    @Override
    public Subscription[] getSubscriptions() {
        return null;
    }

    @Override
    public List<ListItem> getInfo() {
        return null;
    }

    @Override
    public String getCardName() {
        return "Suica"; // FIXME: Could be ICOCA, etc.
    }

    public void writeToParcel(Parcel parcel, int flags) {
        parcel.writeInt(mTrips.length);
        parcel.writeTypedArray(mTrips, flags);
    }

    public static class SuicaTrip extends Trip {
        private final long mBalance;

        private final int mConsoleType;
        private final int mProcessType;

        private final boolean mIsProductSale;
        private final boolean mIsBus;

        private final boolean mIsCharge;

        private final long mFare;
        private final Date mTimestamp;
        private final int mRegionCode;

        private int mRailEntranceLineCode;
        private int mRailEntranceStationCode;
        private int mRailExitLineCode;
        private int mRailExitStationCode;

        private int mBusLineCode;
        private int mBusStopCode;

        private Station mStartStation;
        private Station mEndStation;

        public SuicaTrip(FelicaBlock block, long previousBalance) {
            byte[] data = block.getData();

            // 00000080000000000000000000000000
            // 00 00 - console type
            // 01 00 - process type
            // 02 00 - ??
            // 03 80 - ??
            // 04 00 - date
            // 05 00 - date
            // 06 00 - enter line code
            // 07 00
            // 08 00
            // 09 00
            // 10 00
            // 11 00
            // 12 00
            // 13 00
            // 14 00
            // 15 00

            mConsoleType = data[0];
            mProcessType = data[1];

            mIsBus = mConsoleType == (byte) 0x05;
            mIsProductSale = (mConsoleType == (byte) 0xc7 || mConsoleType == (byte) 0xc8);
            mIsCharge = (mProcessType == (byte) 0x02);

            mTimestamp = extractDate(mIsProductSale, data);
            mBalance = (long) Util.toInt(data[11], data[10]);

            mRegionCode = data[15] & 0xFF;

            if (previousBalance >= 0) {
                mFare = (previousBalance - mBalance);
            } else {
                // Can't get amount for first record.
                mFare = 0;
            }

            // Unused block (new card)
            if (mTimestamp == null) {
                return;
            }

            if (!mIsProductSale && !mIsCharge) {
                if (mIsBus) {
                    mBusLineCode = Util.toInt(data[6], data[7]);
                    mBusStopCode = Util.toInt(data[8], data[9]);
                    mStartStation = getBusStop(mRegionCode, mBusLineCode, mBusStopCode);

                } else {
                    mRailEntranceLineCode = data[6] & 0xFF;
                    mRailEntranceStationCode = data[7] & 0xFF;
                    mRailExitLineCode = data[8] & 0xFF;
                    mRailExitStationCode = data[9] & 0xFF;
                    mStartStation = getRailStation(mRegionCode, mRailEntranceLineCode, mRailEntranceStationCode);
                    mEndStation = getRailStation(mRegionCode, mRailExitLineCode, mRailExitStationCode);
                }
            }
        }

        public static Creator<SuicaTrip> CREATOR = new Creator<SuicaTrip>() {
            public SuicaTrip createFromParcel(Parcel parcel) {
                return new SuicaTrip(parcel);
            }

            public SuicaTrip[] newArray(int size) {
                return new SuicaTrip[size];
            }
        };

        public SuicaTrip(Parcel parcel) {
            mBalance = parcel.readLong();

            mConsoleType = parcel.readInt();
            mProcessType = parcel.readInt();

            mIsProductSale = (parcel.readInt() == 1);
            mIsBus = (parcel.readInt() == 1);

            mIsCharge = (parcel.readInt() == 1);

            mFare = parcel.readLong();
            mTimestamp = new Date(parcel.readLong());
            mRegionCode = parcel.readInt();

            mRailEntranceLineCode = parcel.readInt();
            mRailEntranceStationCode = parcel.readInt();
            mRailExitLineCode = parcel.readInt();
            mRailExitStationCode = parcel.readInt();

            mBusLineCode = parcel.readInt();
            mBusStopCode = parcel.readInt();

            if (parcel.readInt() == 1)
                mStartStation = parcel.readParcelable(Station.class.getClassLoader());
            if (parcel.readInt() == 1)
                mEndStation = parcel.readParcelable(Station.class.getClassLoader());
        }

        @Override
        public long getTimestamp() {
            if (mTimestamp != null)
                return mTimestamp.getTime() / 1000;
            else
                return 0;
        }

        @Override
        public long getExitTimestamp() {
            return 0;
        }

        public boolean hasTime() {
            return mIsProductSale;
        }

        @Override
        public String getRouteName() {
            return (mStartStation != null) ? mStartStation.getLineName()
                    : (getConsoleType() + " " + getProcessType());
        }

        @Override
        public String getAgencyName() {
            return (mStartStation != null) ? mStartStation.getCompanyName() : null;
        }

        @Override
        public String getShortAgencyName() {
            return getAgencyName();
        }

        @Override
        public double getFare() {
            return mFare;
        }

        @Override
        public String getFareString() {
            NumberFormat format = NumberFormat.getCurrencyInstance(Locale.JAPAN);
            format.setMaximumFractionDigits(0);
            if (mFare < 0)
                return "+" + format.format(-mFare);
            else
                return format.format(mFare);
        }

        public long getBalance() {
            return mBalance;
        }

        @Override
        public String getBalanceString() {
            NumberFormat format = NumberFormat.getCurrencyInstance(Locale.JAPAN);
            format.setMaximumFractionDigits(0);
            return format.format(mBalance);
        }

        @Override
        public String getStartStationName() {
            if (mIsProductSale || mIsCharge)
                return null;

            if (mStartStation != null) {
                return mStartStation.getShortStationName();
            }
            if (mIsBus) {
                return String.format("Bus Area 0x%s Line 0x%s Stop 0x%s", Integer.toHexString(mRegionCode),
                        Integer.toHexString(mBusLineCode), Integer.toHexString(mBusStopCode));
            } else if (!(mRailEntranceLineCode == 0 && mRailEntranceStationCode == 0)) {
                return String.format("Line 0x%s Station 0x%s", Integer.toHexString(mRailEntranceLineCode),
                        Integer.toHexString(mRailEntranceStationCode));
            } else {
                return null;
            }
        }

        @Override
        public Station getStartStation() {
            return mStartStation;
        }

        @Override
        public String getEndStationName() {
            if (mIsProductSale || mIsCharge || isTVM())
                return null;

            if (mEndStation != null) {
                return mEndStation.getShortStationName();
            }
            if (!mIsBus) {
                return String.format("Line 0x%s Station 0x%s", Integer.toHexString(mRailExitLineCode),
                        Integer.toHexString(mRailExitStationCode));
            }
            return null;
        }

        @Override
        public Station getEndStation() {
            return mEndStation;
        }

        @Override
        public Mode getMode() {
            int consoleType = mConsoleType & 0xFF;
            if (isTVM()) {
                return Mode.TICKET_MACHINE;
            } else if (consoleType == 0xc8) {
                return Mode.VENDING_MACHINE;
            } else if (consoleType == 0xc7) {
                return Mode.POS;
            } else if (mIsBus) {
                return Mode.BUS;
            } else {
                return Mode.METRO;
            }
        }

        public String getConsoleType() {
            return SuicaTransitData.getConsoleTypeName(mConsoleType);
        }

        public String getProcessType() {
            return SuicaTransitData.getProcessTypeName(mProcessType);
        }

        /*
        public boolean isBus() {
        return mIsBus;
        }
            
        public boolean isProductSale() {
        return mIsProductSale;
        }
            
        public boolean isCharge() {
        return mIsCharge;
        }
            
        public int getRegionCode() {
        return mRegionCode;
        }
            
        public int getBusLineCode() {
        return mBusLineCode;
        }
            
        public int getBusStopCode() {
        return mBusStopCode;
        }
            
        public int getRailEntranceLineCode() {
        return mRailEntranceLineCode;
        }
            
        public int getRailEntranceStationCode() {
        return mRailEntranceStationCode;
        }
            
        public int getRailExitLineCode() {
        return mRailExitLineCode;
        }
            
        public int getRailExitStationCode() {
        return mRailExitStationCode;
        }
        */

        public void writeToParcel(Parcel parcel, int flags) {
            parcel.writeLong(mBalance);

            parcel.writeInt(mConsoleType);
            parcel.writeInt(mProcessType);

            parcel.writeInt(mIsProductSale ? 1 : 0);
            parcel.writeInt(mIsBus ? 1 : 0);

            parcel.writeInt(mIsCharge ? 1 : 0);

            parcel.writeLong(mFare);
            parcel.writeLong(mTimestamp.getTime());
            parcel.writeInt(mRegionCode);

            parcel.writeInt(mRailEntranceLineCode);
            parcel.writeInt(mRailEntranceStationCode);
            parcel.writeInt(mRailExitLineCode);
            parcel.writeInt(mRailExitStationCode);

            parcel.writeInt(mBusLineCode);
            parcel.writeInt(mBusStopCode);

            if (mStartStation != null) {
                parcel.writeInt(1);
                parcel.writeParcelable(mStartStation, flags);
            } else {
                parcel.writeInt(0);
            }

            if (mEndStation != null) {
                parcel.writeInt(1);
                parcel.writeParcelable(mEndStation, flags);
            } else {
                parcel.writeInt(0);
            }
        }

        public int describeContents() {
            return 0;
        }

        private boolean isTVM() {
            int consoleType = mConsoleType & 0xFF;
            int[] tvmConsoleTypes = { 0x03, 0x07, 0x08, 0x12, 0x13, 0x14, 0x15 };
            return ArrayUtils.contains(tvmConsoleTypes, consoleType);
        }
    }

    private static Date extractDate(boolean isProductSale, byte[] data) {
        int date = Util.toInt(data[4], data[5]);
        if (date == 0)
            return null;
        int yy = date >> 9;
        int mm = (date >> 5) & 0xf;
        int dd = date & 0x1f;
        Calendar c = Calendar.getInstance();
        c.set(Calendar.YEAR, 2000 + yy);
        c.set(Calendar.MONTH, mm - 1);
        c.set(Calendar.DAY_OF_MONTH, dd);

        // Product sales have time, too.
        // ???
        if (isProductSale) {
            int time = Util.toInt(data[6], data[7]);
            int hh = time >> 11;
            int min = (time >> 5) & 0x3f;
            c.set(Calendar.HOUR_OF_DAY, hh);
            c.set(Calendar.MINUTE, min);
        } else {
            c.set(Calendar.HOUR_OF_DAY, 0);
            c.set(Calendar.MINUTE, 0);
        }
        return c.getTime();
    }

    /**
     * ????
     * <pre>http:// sourceforge.jp/projects/felicalib/wiki/suica???????</pre>
     * @param cType 
     * @return String ????
     */
    private static String getConsoleTypeName(int cType) {
        Application app = FareBotApplication.getInstance();
        switch (cType & 0xff) {
        case 0x03:
            return app.getString(R.string.felica_terminal_fare_adjustment);
        case 0x04:
            return app.getString(R.string.felica_terminal_portable);
        case 0x05:
            return app.getString(R.string.felica_terminal_vehicle); // bus
        case 0x07:
            return app.getString(R.string.felica_terminal_ticket);
        case 0x08:
            return app.getString(R.string.felica_terminal_ticket);
        case 0x09:
            return app.getString(R.string.felica_terminal_deposit_quick_charge);
        case 0x12:
            return app.getString(R.string.felica_terminal_tvm_tokyo_monorail);
        case 0x13:
            return app.getString(R.string.felica_terminal_tvm_etc);
        case 0x14:
            return app.getString(R.string.felica_terminal_tvm_etc);
        case 0x15:
            return app.getString(R.string.felica_terminal_tvm_etc);
        case 0x16:
            return app.getString(R.string.felica_terminal_ticket_gate);
        case 0x17:
            return app.getString(R.string.felica_terminal_simple_ticket_gate);
        case 0x18:
            return app.getString(R.string.felica_terminal_booth);
        case 0x19:
            return app.getString(R.string.felica_terminal_booth_green);
        case 0x1a:
            return app.getString(R.string.felica_terminal_ticket_gate_terminal);
        case 0x1b:
            return app.getString(R.string.felica_terminal_mobile_phone);
        case 0x1c:
            return app.getString(R.string.felica_terminal_connection_adjustment);
        case 0x1d:
            return app.getString(R.string.felica_terminal_transfer_adjustment);
        case 0x1f:
            return app.getString(R.string.felica_terminal_simple_deposit);
        case 0x46:
            return "VIEW ALTTE";
        case 0x48:
            return "VIEW ALTTE";
        case 0xc7:
            return app.getString(R.string.felica_terminal_pos); // sales
        case 0xc8:
            return app.getString(R.string.felica_terminal_vending); // sales
        default:
            return String.format("Console 0x%s", Integer.toHexString(cType));
        }
    }

    /**
     * ?????
     * <pre>http:// sourceforge.jp/projects/felicalib/wiki/suica???????</pre>
     * @param proc ?
     * @return String ?????
     */
    private static String getProcessTypeName(int proc) {
        Application app = FareBotApplication.getInstance();
        switch (proc & 0xff) {
        case 0x01:
            return app.getString(R.string.felica_process_fare_exit_gate);
        case 0x02:
            return app.getString(R.string.felica_process_charge);
        case 0x03:
            return app.getString(R.string.felica_process_purchase_magnetic);
        case 0x04:
            return app.getString(R.string.felica_process_fare_adjustment);
        case 0x05:
            return app.getString(R.string.felica_process_admission_payment);
        case 0x06:
            return app.getString(R.string.felica_process_booth_exit);
        case 0x07:
            return app.getString(R.string.felica_process_issue_new);
        case 0x08:
            return app.getString(R.string.felica_process_booth_deduction);
        case 0x0d:
            return app.getString(R.string.felica_process_bus_pitapa); // Bus
        case 0x0f:
            return app.getString(R.string.felica_process_bus_iruca); // Bus
        case 0x11:
            return app.getString(R.string.felica_process_reissue);
        case 0x13:
            return app.getString(R.string.felica_process_payment_shinkansen);
        case 0x14:
            return app.getString(R.string.felica_process_entry_a_autocharge);
        case 0x15:
            return app.getString(R.string.felica_process_exit_a_autocharge);
        case 0x1f:
            return app.getString(R.string.felica_process_deposit_bus); // Bus
        case 0x23:
            return app.getString(R.string.felica_process_purchase_special_ticket); // Bus
        case 0x46:
            return app.getString(R.string.felica_process_merchandise_purchase); // Sales
        case 0x48:
            return app.getString(R.string.felica_process_bonus_charge);
        case 0x49:
            return app.getString(R.string.felica_process_register_deposit); // Sales
        case 0x4a:
            return app.getString(R.string.felica_process_merchandise_cancel); // Sales
        case 0x4b:
            return app.getString(R.string.felica_process_merchandise_admission); // Sales
        case 0xc6:
            return app.getString(R.string.felica_process_merchandise_purchase_cash); // Sales
        case 0xcb:
            return app.getString(R.string.felica_process_merchandise_admission_cash); // Sales
        case 0x84:
            return app.getString(R.string.felica_process_payment_thirdparty);
        case 0x85:
            return app.getString(R.string.felica_process_admission_thirdparty);
        default:
            return String.format("Process0x%s", Integer.toHexString(proc));
        }
    }

    /**
     * ?????
     * <pre>http:// sourceforge.jp/projects/felicalib/wiki/suica???????</pre>
     * @param lineCode 
     * @param stationCode 
     * @return ????????0????1??????
     */
    private static Station getBusStop(int regionCode, int lineCode, int stationCode) {
        int areaCode = (regionCode >> 6);

        try {
            SQLiteDatabase db = FareBotApplication.getInstance().getSuicaDBUtil().openDatabase();
            Cursor cursor = db.query(TABLE_IRUCA_STATIONCODE, COLUMNS_IRUCA_STATIONCODE,
                    String.format("%s = ? AND %s = ?", COLUMN_LINECODE, COLUMN_STATIONCODE),
                    new String[] { Integer.toHexString(lineCode), Integer.toHexString(stationCode) }, null, null,
                    COLUMN_ID);

            if (!cursor.moveToFirst()) {
                return null;
            }

            // FIXME: Figure out a better way to deal with i18n.
            boolean isJa = Locale.getDefault().getLanguage().equals("ja");
            String companyName = cursor
                    .getString(cursor.getColumnIndex(isJa ? COLUMN_COMPANYNAME : COLUMN_COMPANYNAME_EN));
            String stationName = cursor
                    .getString(cursor.getColumnIndex(isJa ? COLUMN_STATIONNAME : COLUMN_STATIONNAME_EN));
            return new Station(companyName, null, stationName, null, null, null);

        } catch (Exception e) {
            Log.e("SuicaStationProvider", "getBusStop() error", e);
            return null;
        }
    }

    /**
     * ?????????
     * <pre>http://sourceforge.jp/projects/felicalib/wiki/suica???????</pre>
     *
     * @param regionCode 
     * @param lineCode 
     * @param stationCode 
     * @return ????????0????1????2??????
     */
    private static Station getRailStation(int regionCode, int lineCode, int stationCode) {
        int areaCode = (regionCode >> 6);

        try {
            SQLiteDatabase db = FareBotApplication.getInstance().getSuicaDBUtil().openDatabase();
            Cursor cursor = db.query(TABLE_STATIONCODE, COLUMNS_STATIONCODE,
                    String.format("%s = ? AND %s = ? AND %s = ?", COLUMN_AREACODE, COLUMN_LINECODE,
                            COLUMN_STATIONCODE),
                    new String[] { String.valueOf(areaCode & 0xFF), String.valueOf(lineCode & 0xFF),
                            String.valueOf(stationCode & 0xFF) },
                    null, null, COLUMN_ID);

            if (!cursor.moveToFirst()) {
                Log.w("SuicaTransitData",
                        String.format("FAILED get rail company: r: 0x%s a: 0x%s l: 0x%s s: 0x%s",
                                Integer.toHexString(regionCode), Integer.toHexString(areaCode),
                                Integer.toHexString(lineCode), Integer.toHexString(stationCode)));

                return null;
            }

            // FIXME: Figure out a better way to deal with i18n.
            boolean isJa = Locale.getDefault().getLanguage().equals("ja");
            String companyName = cursor
                    .getString(cursor.getColumnIndex(isJa ? COLUMN_COMPANYNAME : COLUMN_COMPANYNAME_EN));
            String lineName = cursor.getString(cursor.getColumnIndex(isJa ? COLUMN_LINENAME : COLUMN_LINENAME_EN));
            String stationName = cursor
                    .getString(cursor.getColumnIndex(isJa ? COLUMN_STATIONNAME : COLUMN_STATIONNAME_EN));
            String latitude = cursor.getString(cursor.getColumnIndex(COLUMN_LATITUDE));
            String longitude = cursor.getString(cursor.getColumnIndex(COLUMN_LONGITUDE));
            return new Station(companyName, lineName, stationName, null, latitude, longitude);

        } catch (Exception e) {
            Log.e("SuicaStationProvider", "Error in getRailStation", e);
            return null;
        }
    }
}