/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * 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.android.smspush;

import android.app.Service;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.RemoteException;
import android.util.Log;

import com.android.internal.telephony.IWapPushManager;
import com.android.internal.telephony.WapPushManagerParams;

/**
 * The WapPushManager service is implemented to process incoming
 * WAP Push messages and to maintain the Receiver Application/Application
 * ID mapping. The WapPushManager runs as a system service, and only the
 * WapPushManager can update the WAP Push message and Receiver Application
 * mapping (Application ID Table). When the device receives an SMS WAP Push
 * message, the WapPushManager looks up the Receiver Application name in
 * Application ID Table. If an application is found, the application is
 * launched using its full component name instead of broadcasting an implicit
 * Intent. If a Receiver Application is not found in the Application ID
 * Table or the WapPushManager returns a process-further value, the
 * telephony stack will process the message using existing message processing
 * flow, and broadcast an implicit Intent.
 */
public class WapPushManager extends Service {

    private static final String LOG_TAG = "WAP PUSH";
    private static final String DATABASE_NAME = "wappush.db";
    private static final String APPID_TABLE_NAME = "appid_tbl";

    /**
     * Version number must be incremented when table structure is changed.
     */
    private static final int WAP_PUSH_MANAGER_VERSION = 1;
    private static final boolean DEBUG_SQL = false;
    private static final boolean LOCAL_LOGV = false;

    /**
     * Inner class that deals with application ID table
     */
    private class WapPushManDBHelper extends SQLiteOpenHelper {
        WapPushManDBHelper(Context context) {
            super(context, DATABASE_NAME, null, WAP_PUSH_MANAGER_VERSION);
            if (LOCAL_LOGV) Log.v(LOG_TAG, "helper instance created.");
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            if (LOCAL_LOGV) Log.v(LOG_TAG, "db onCreate.");
            String sql = "CREATE TABLE " + APPID_TABLE_NAME + " ("
                    + "id INTEGER PRIMARY KEY, "
                    + "x_wap_application TEXT, "
                    + "content_type TEXT, "
                    + "package_name TEXT, "
                    + "class_name TEXT, "
                    + "app_type INTEGER, "
                    + "need_signature INTEGER, "
                    + "further_processing INTEGER, "
                    + "install_order INTEGER "
                    + ")";

            if (DEBUG_SQL) Log.v(LOG_TAG, "sql: " + sql);
            db.execSQL(sql);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db,
                    int oldVersion, int newVersion) {
            // TODO: when table structure is changed, need to dump and restore data.
            /*
              db.execSQL(
              "drop table if exists "+APPID_TABLE_NAME);
              onCreate(db);
            */
            Log.w(LOG_TAG, "onUpgrade is not implemented yet. do nothing.");
        }

        protected class queryData {
            public String packageName;
            public String className;
            int appType;
            int needSignature;
            int furtherProcessing;
            int installOrder;
        }

        /**
         * Query the latest receiver application info with supplied application ID and
         * content type.
         * @param app_id    application ID to look up
         * @param content_type    content type to look up
         */
        protected queryData queryLastApp(SQLiteDatabase db,
                String app_id, String content_type) {
            if (LOCAL_LOGV) Log.v(LOG_TAG, "queryLastApp app_id: " + app_id
                    + " content_type: " +  content_type);

            Cursor cur = db.query(APPID_TABLE_NAME,
                    new String[] {"install_order", "package_name", "class_name",
                    "app_type", "need_signature", "further_processing"},
                    "x_wap_application=? and content_type=?",
                    new String[] {app_id, content_type},
                    null /* groupBy */,
                    null /* having */,
                    "install_order desc" /* orderBy */);

            queryData ret = null;

            if (cur.moveToNext()) {
                ret = new queryData();
                ret.installOrder = cur.getInt(cur.getColumnIndex("install_order"));
                ret.packageName = cur.getString(cur.getColumnIndex("package_name"));
                ret.className = cur.getString(cur.getColumnIndex("class_name"));
                ret.appType = cur.getInt(cur.getColumnIndex("app_type"));
                ret.needSignature = cur.getInt(cur.getColumnIndex("need_signature"));
                ret.furtherProcessing = cur.getInt(cur.getColumnIndex("further_processing"));
            }
            cur.close();
            return ret;
        }

    }

    /**
     * The exported API implementations class
     */
    private class IWapPushManagerStub extends IWapPushManager.Stub {
        public Context mContext;

        public IWapPushManagerStub() {

        }

        /**
         * Compare the package signature with WapPushManager package
         */
        protected boolean signatureCheck(String package_name) {
            PackageManager pm = mContext.getPackageManager();
            int match = pm.checkSignatures(mContext.getPackageName(), package_name);

            if (LOCAL_LOGV) Log.v(LOG_TAG, "compare signature " + mContext.getPackageName()
                    + " and " +  package_name + ", match=" + match);

            return match == PackageManager.SIGNATURE_MATCH;
        }

        /**
         * Returns the status value of the message processing.
         * The message will be processed as follows:
         * 1.Look up Application ID Table with x-wap-application-id + content type
         * 2.Check the signature of package name that is found in the
         *   Application ID Table by using PackageManager.checkSignature
         * 3.Trigger the Application
         * 4.Returns the process status value.
         */
        public int processMessage(String app_id, String content_type, Intent intent)
            throws RemoteException {
            Log.d(LOG_TAG, "wpman processMsg " + app_id + ":" + content_type);

            WapPushManDBHelper dbh = getDatabase(mContext);
            SQLiteDatabase db = dbh.getReadableDatabase();
            WapPushManDBHelper.queryData lastapp = dbh.queryLastApp(db, app_id, content_type);
            db.close();

            if (lastapp == null) {
                Log.w(LOG_TAG, "no receiver app found for " + app_id + ":" + content_type);
                return WapPushManagerParams.APP_QUERY_FAILED;
            }
            if (LOCAL_LOGV) Log.v(LOG_TAG, "starting " + lastapp.packageName
                    + "/" + lastapp.className);

            if (lastapp.needSignature != 0) {
                if (!signatureCheck(lastapp.packageName)) {
                    return WapPushManagerParams.SIGNATURE_NO_MATCH;
                }
            }

            if (lastapp.appType == WapPushManagerParams.APP_TYPE_ACTIVITY) {
                //Intent intent = new Intent(Intent.ACTION_MAIN);
                intent.setClassName(lastapp.packageName, lastapp.className);
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

                try {
                    mContext.startActivity(intent);
                } catch (ActivityNotFoundException e) {
                    Log.w(LOG_TAG, "invalid name " +
                            lastapp.packageName + "/" + lastapp.className);
                    return WapPushManagerParams.INVALID_RECEIVER_NAME;
                }
            } else {
                intent.setClassName(mContext, lastapp.className);
                intent.setComponent(new ComponentName(lastapp.packageName,
                        lastapp.className));
                PackageManager pm = mContext.getPackageManager();
                PowerManager powerManager =
                        (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
                try {
                    ApplicationInfo appInfo = pm.getApplicationInfo(lastapp.packageName, 0);
                    if (appInfo.targetSdkVersion < Build.VERSION_CODES.O ||
                            powerManager.isIgnoringBatteryOptimizations(lastapp.packageName)) {
                        if (mContext.startService(intent) == null) {
                            Log.w(LOG_TAG, "invalid name " +
                                    lastapp.packageName + "/" + lastapp.className);
                            return WapPushManagerParams.INVALID_RECEIVER_NAME;
                        }
                    } else {
                        if (mContext.startForegroundService(intent) == null) {
                            Log.w(LOG_TAG, "invalid name " +
                                    lastapp.packageName + "/" + lastapp.className);
                            return WapPushManagerParams.INVALID_RECEIVER_NAME;
                        }
                    }

                } catch (NameNotFoundException e) {
                    Log.w(LOG_TAG, "invalid name " +
                            lastapp.packageName + "/" + lastapp.className);
                    return WapPushManagerParams.INVALID_RECEIVER_NAME;
                }
            }

            return WapPushManagerParams.MESSAGE_HANDLED
                    | (lastapp.furtherProcessing == 1 ?
                            WapPushManagerParams.FURTHER_PROCESSING : 0);
        }

        protected boolean appTypeCheck(int app_type) {
            if (app_type == WapPushManagerParams.APP_TYPE_ACTIVITY ||
                    app_type == WapPushManagerParams.APP_TYPE_SERVICE) {
                return true;
            } else {
                return false;
            }
        }

        /**
         * Returns true if adding the package succeeded.
         */
        public boolean addPackage(String x_app_id, String content_type,
                String package_name, String class_name,
                int app_type, boolean need_signature, boolean further_processing) {
            WapPushManDBHelper dbh = getDatabase(mContext);
            SQLiteDatabase db = dbh.getWritableDatabase();
            WapPushManDBHelper.queryData lastapp = dbh.queryLastApp(db, x_app_id, content_type);
            boolean ret = false;
            boolean insert = false;
            int sq = 0;

            if (!appTypeCheck(app_type)) {
                Log.w(LOG_TAG, "invalid app_type " + app_type + ". app_type must be "
                        + WapPushManagerParams.APP_TYPE_ACTIVITY + " or "
                        + WapPushManagerParams.APP_TYPE_SERVICE);
                return false;
            }

            if (lastapp == null) {
                insert = true;
                sq = 0;
            } else if (!lastapp.packageName.equals(package_name) ||
                    !lastapp.className.equals(class_name)) {
                insert = true;
                sq = lastapp.installOrder + 1;
            }

            if (insert) {
                ContentValues values = new ContentValues();

                values.put("x_wap_application", x_app_id);
                values.put("content_type", content_type);
                values.put("package_name", package_name);
                values.put("class_name", class_name);
                values.put("app_type", app_type);
                values.put("need_signature", need_signature ? 1 : 0);
                values.put("further_processing", further_processing ? 1 : 0);
                values.put("install_order", sq);
                db.insert(APPID_TABLE_NAME, null, values);
                if (LOCAL_LOGV) Log.v(LOG_TAG, "add:" + x_app_id + ":" + content_type
                        + " " + package_name + "." + class_name
                        + ", newsq:" + sq);
                ret = true;
            }

            db.close();

            return ret;
        }

        /**
         * Returns true if updating the package succeeded.
         */
        public boolean updatePackage(String x_app_id, String content_type,
                String package_name, String class_name,
                int app_type, boolean need_signature, boolean further_processing) {

            if (!appTypeCheck(app_type)) {
                Log.w(LOG_TAG, "invalid app_type " + app_type + ". app_type must be "
                        + WapPushManagerParams.APP_TYPE_ACTIVITY + " or "
                        + WapPushManagerParams.APP_TYPE_SERVICE);
                return false;
            }

            WapPushManDBHelper dbh = getDatabase(mContext);
            SQLiteDatabase db = dbh.getWritableDatabase();
            WapPushManDBHelper.queryData lastapp = dbh.queryLastApp(db, x_app_id, content_type);

            if (lastapp == null) {
                db.close();
                return false;
            }

            ContentValues values = new ContentValues();
            String where = "x_wap_application=\'" + x_app_id + "\'"
                    + " and content_type=\'" + content_type + "\'"
                    + " and install_order=" + lastapp.installOrder;

            values.put("package_name", package_name);
            values.put("class_name", class_name);
            values.put("app_type", app_type);
            values.put("need_signature", need_signature ? 1 : 0);
            values.put("further_processing", further_processing ? 1 : 0);

            int num = db.update(APPID_TABLE_NAME, values, where, null);
            if (LOCAL_LOGV) Log.v(LOG_TAG, "update:" + x_app_id + ":" + content_type + " "
                    + package_name + "." + class_name
                    + ", sq:" + lastapp.installOrder);

            db.close();

            return num > 0;
        }

        /**
         * Returns true if deleting the package succeeded.
         */
        public boolean deletePackage(String x_app_id, String content_type,
                String package_name, String class_name) {
            WapPushManDBHelper dbh = getDatabase(mContext);
            SQLiteDatabase db = dbh.getWritableDatabase();
            String where = "x_wap_application=\'" + x_app_id + "\'"
                    + " and content_type=\'" + content_type + "\'"
                    + " and package_name=\'" + package_name + "\'"
                    + " and class_name=\'" + class_name + "\'";
            int num_removed = db.delete(APPID_TABLE_NAME, where, null);

            db.close();
            if (LOCAL_LOGV) Log.v(LOG_TAG, "deleted " + num_removed + " rows:"
                    + x_app_id + ":" + content_type + " "
                    + package_name + "." + class_name);
            return num_removed > 0;
        }
    };


    /**
     * Linux IPC Binder
     */
    private final IWapPushManagerStub mBinder = new IWapPushManagerStub();

    /**
     * Default constructor
     */
    public WapPushManager() {
        super();
        mBinder.mContext = this;
    }

    @Override
    public IBinder onBind(Intent arg0) {
        return mBinder;
    }

    /**
     * Application ID database instance
     */
    private WapPushManDBHelper mDbHelper = null;
    protected WapPushManDBHelper getDatabase(Context context) {
        if (mDbHelper == null) {
            if (LOCAL_LOGV) Log.v(LOG_TAG, "create new db inst.");
            mDbHelper = new WapPushManDBHelper(context);
        }
        return mDbHelper;
    }


    /**
     * This method is used for testing
     */
    public boolean verifyData(String x_app_id, String content_type,
            String package_name, String class_name,
            int app_type, boolean need_signature, boolean further_processing) {
        WapPushManDBHelper dbh = getDatabase(this);
        SQLiteDatabase db = dbh.getReadableDatabase();
        WapPushManDBHelper.queryData lastapp = dbh.queryLastApp(db, x_app_id, content_type);

        if (LOCAL_LOGV) Log.v(LOG_TAG, "verifyData app id: " + x_app_id + " content type: " +
                content_type + " lastapp: " + lastapp);

        db.close();

        if (lastapp == null) return false;

        if (LOCAL_LOGV) Log.v(LOG_TAG, "verifyData lastapp.packageName: " + lastapp.packageName +
                " lastapp.className: " + lastapp.className +
                " lastapp.appType: " + lastapp.appType +
                " lastapp.needSignature: " + lastapp.needSignature +
                " lastapp.furtherProcessing: " + lastapp.furtherProcessing);


        if (lastapp.packageName.equals(package_name)
                && lastapp.className.equals(class_name)
                && lastapp.appType == app_type
                &&  lastapp.needSignature == (need_signature ? 1 : 0)
                &&  lastapp.furtherProcessing == (further_processing ? 1 : 0)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * This method is used for testing
     */
    public boolean isDataExist(String x_app_id, String content_type,
            String package_name, String class_name) {
        WapPushManDBHelper dbh = getDatabase(this);
        SQLiteDatabase db = dbh.getReadableDatabase();
        boolean ret = dbh.queryLastApp(db, x_app_id, content_type) != null;

        db.close();
        return ret;
    }

}

