package guard.api.sensitive.ui.sapiguard;

import android.app.Activity;
import android.app.ActivityManager;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.os.Binder;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Process;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import guard.api.sensitive.ui.sapiguard.hookedapis.XActivityManagerService;
import guard.api.sensitive.ui.sapiguard.utils.PrivacyManager;
import guard.api.sensitive.ui.sapiguard.utils.Util;


public class PackageService extends IPackageService.Stub {
    private static IPackageService mClient = null;
    private static final int cCurrentVersion = 481;
    private static Context mContext;
    private static Semaphore mOndemandSemaphore = new Semaphore(1, true);
    private static Thread mWorker = null;
    private static Handler mHandler = null;
    private SQLiteDatabase mDb = null;
    private boolean mCorrupt = false;
    private ReentrantReadWriteLock mLock = new ReentrantReadWriteLock(true);
    private static final String cTableAPITrace = "apitrace";

    private static final String cServiceName = "spaiguard481";
    private static PackageService mPackageService = null;
    private static Object mAm;
    private static String mSecret = null;
    private static List<String> mListError = new ArrayList<String>();
    private static String hookpkgName = "-h-o-o-k-";

    private PackageService() {
    }

    @Override
    public String getPkgName() throws RemoteException {
        return hookpkgName;
    }

    @Override
    public void setPkgName(String pkgname) throws RemoteException {
        Log.d("jie-setPkgName", pkgname);
        hookpkgName = pkgname;
    }


    public static IPackageService getClient() {
        // Runs client side
        if (mClient == null)
            try {
                // public static IBinder getService(String name)
                Class<?> cServiceManager = Class.forName("android.os.ServiceManager");
                Method mGetService = cServiceManager.getDeclaredMethod("getService", String.class);
                mClient = IPackageService.Stub.asInterface((IBinder) mGetService.invoke(null, getServiceName()));
            } catch (Throwable ex) {
                Util.bug(null, ex);
            }

        return mClient;
    }

    private static String getServiceName() {
        return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? "user." : "") + cServiceName;
    }


    public static void register(List<String> listError, ClassLoader classLoader, String secret, Object am) {
        // Store secret and errors
        mAm = am;
        mSecret = secret;
        mListError.addAll(listError);

        try {
            // Register privacy service
            mPackageService = new PackageService();

            // @formatter:off
            // public static void addService(String name, IBinder service)
            // public static void addService(String name, IBinder service, boolean allowIsolated)
            // @formatter:on

            // Requires this in /service_contexts
            // sapiguard453 u:object_r:system_server_service:s0

            Class<?> cServiceManager = Class.forName("android.os.ServiceManager", false, classLoader);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                Method mAddService = cServiceManager.getDeclaredMethod("addService", String.class, IBinder.class,
                        boolean.class);
                mAddService.invoke(null, getServiceName(), mPackageService, true);
            } else {
                Method mAddService = cServiceManager.getDeclaredMethod("addService", String.class, IBinder.class);
                mAddService.invoke(null, getServiceName(), mPackageService);
            }

            // This will and should open the database
            Util.log(null, Log.WARN, "Service registered name=" + getServiceName() + " version=" + cCurrentVersion);

            // Publish semaphore to activity manager service
            XActivityManagerService.setSemaphore(mOndemandSemaphore);

            // Get context
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                Field fContext = null;
                Class<?> cam = am.getClass();
                while (cam != null && fContext == null)
                    try {
                        fContext = cam.getDeclaredField("mContext");
                    } catch (NoSuchFieldException ignored) {
                        cam = cam.getSuperclass();
                    }

                if (fContext == null)
                    Util.log(null, Log.ERROR, am.getClass().getName() + ".mContext not found");
                else {
                    fContext.setAccessible(true);
                    mContext = (Context) fContext.get(am);

                }
            }

            // Start a worker thread
            mWorker = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Looper.prepare();
                        mHandler = new Handler();
                        Looper.loop();
                    } catch (Throwable ex) {
                        Util.bug(null, ex);
                    }
                }
            });
            mWorker.start();
        } catch (Throwable ex) {
            Util.bug(null, ex);
        }
    }

    public String getTopActivityName() {
        Activity activity = PrivacyManager.getTopActivity();
        if (activity!=null)
            return  activity.getComponentName().getClassName();


        final Context context = getContext();

        if (context==null)
            return null;


        ActivityManager mActivityManager =
                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        if (mActivityManager.getRunningTasks(1) == null) {
            Log.e("jie-gettopaty", "running task is null, ams is abnormal!!!");
            return null;
        }
        ActivityManager.RunningTaskInfo mRunningTask =
                mActivityManager.getRunningTasks(1).get(0);
        if (mRunningTask == null) {
            Log.e("jie-gettopaty", "failed to get RunningTaskInfo");
            return null;
        }

        String activityName =  mRunningTask.topActivity.getClassName();
        return activityName;
    }

    private Context getContext() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
            return mContext;
        else {
            // public static ActivityManagerService self()
            // frameworks/base/services/java/com/android/server/am/ActivityManagerService.java
            try {
                Class<?> cam = Class.forName("com.android.server.am.ActivityManagerService");
                Object am = cam.getMethod("self").invoke(null);
                if (am == null)
                    return null;
                Field mContext = cam.getDeclaredField("mContext");
                mContext.setAccessible(true);
                return (Context) mContext.get(am);
            } catch (Throwable ex) {
                Util.bug(null, ex);
                return null;
            }
        }
    }



    private File getDbFile() {
        return new File(Environment.getDataDirectory() + File.separator + "system" + File.separator + "sapiguard"
                + File.separator + "sapiguard.db");
//        return new File(Environment.getExternalStorageDirectory()+ File.separator + "sapiguard"+ File.separator + "sapiguard.db");
    }

    private void setupDatabase() {
        try {
            File dbFile = getDbFile();
            Log.d("jie-file", dbFile.getPath());
            // Create database folder
            dbFile.getParentFile().mkdirs();

            // Check database folder
            if (dbFile.getParentFile().isDirectory())
                Util.log(null, Log.WARN, "Database folder=" + dbFile.getParentFile());
            else
                Util.log(null, Log.WARN, "Does not exist folder=" + dbFile.getParentFile());

            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                // Move database from data/sapiguard folder
                File folder = new File(Environment.getDataDirectory() + File.separator + "sapiguard");
                if (folder.exists()) {
                    File[] oldFiles = folder.listFiles();
                    if (oldFiles != null)
                        for (File file : oldFiles)
                            if (file.getName().startsWith("sapiguard.db")) {
                                File target = new File(dbFile.getParentFile() + File.separator + file.getName());
                                boolean status = Util.move(file, target);
                                Util.log(null, Log.WARN, "Moved " + file + " to " + target + " ok=" + status);
                            }
                    Util.log(null, Log.WARN, "Deleting folder=" + folder);
                    folder.delete();
                }

                // Move database from data/application folder
                folder = new File(Environment.getDataDirectory() + File.separator + "data" + File.separator
                        + PackageService.class.getPackage().getName());
                if (folder.exists()) {
                    File[] oldFiles = folder.listFiles();
                    if (oldFiles != null)
                        for (File file : oldFiles)
                            if (file.getName().startsWith("sapiguard.db")) {
                                File target = new File(dbFile.getParentFile() + File.separator + file.getName());
                                boolean status = Util.move(file, target);
                                Util.log(null, Log.WARN, "Moved " + file + " to " + target + " ok=" + status);
                            }
                    Util.log(null, Log.WARN, "Deleting folder=" + folder);
                    folder.delete();
                }
            }

            // Set database file permissions
            // Owner: rwx (system)
            // Group: rwx (system)
            // World: ---
            Util.setPermissions(dbFile.getParentFile().getAbsolutePath(), 0770, Process.SYSTEM_UID, Process.SYSTEM_UID);
            File[] files = dbFile.getParentFile().listFiles();
            if (files != null)
                for (File file : files)
                    if (file.getName().startsWith("sapiguard.db"))
                        Util.setPermissions(file.getAbsolutePath(), 0770, Process.SYSTEM_UID, Process.SYSTEM_UID);

        } catch (Throwable ex) {
            Util.bug(null, ex);
        }
    }

    private SQLiteDatabase getDb() {
        synchronized (this) {
            // Check current reference
            if (mDb != null && !mDb.isOpen()) {
                mDb = null;
                Util.log(null, Log.ERROR, "Database not open");
            }

            if (mDb == null)
                try {
                    setupDatabase();

                    // Create/upgrade database when needed
                    File dbFile = getDbFile();
                    SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(dbFile, null);

                    // Check database integrity
                    if (db.isDatabaseIntegrityOk())
                        Util.log(null, Log.WARN, "Database integrity ok");
                    else {
                        // http://www.sqlite.org/howtocorrupt.html
                        mCorrupt = true;
                        Util.log(null, Log.ERROR, "Database corrupt");
                        Cursor cursor = db.rawQuery("PRAGMA integrity_check", null);
                        try {
                            while (cursor.moveToNext()) {
                                String message = cursor.getString(0);
                                Util.log(null, Log.ERROR, message);
                            }
                        } finally {
                            cursor.close();
                        }
                        db.close();

                        // Backup database file
                        File dbBackup = new File(dbFile.getParentFile() + File.separator + "sapiguard.backup");
                        dbBackup.delete();
                        dbFile.renameTo(dbBackup);

                        File dbJournal = new File(dbFile.getAbsolutePath() + "-journal");
                        File dbJournalBackup = new File(dbBackup.getAbsolutePath() + "-journal");
                        dbJournalBackup.delete();
                        dbJournal.renameTo(dbJournalBackup);

                        Util.log(null, Log.ERROR, "Old database backup: " + dbBackup.getAbsolutePath());

                        // Create new database
                        db = SQLiteDatabase.openOrCreateDatabase(dbFile, null);
                        Util.log(null, Log.ERROR, "New, empty database created");
                    }


                    // Upgrade database if needed
//                    if (db.needUpgrade(1)) {
                    Util.log(null, Log.WARN, "Creating database");
                    mLock.writeLock().lock();
                    try {
                        db.beginTransaction();
                        try {
                            // http://www.sqlite.org/lang_createtable.html
//                                db.execSQL("CREATE TABLE apitrace (id INTEGER primary key autoincrement, stacktrace TEXT NOT NULL, hascode INTEGER  NOT NULL, mtdsig TEXT NOT NULL, times INTEGER NOT NULL)");
//                                db.execSQL("CREATE UNIQUE INDEX idx_apitrace ON apitrace(id, stacktrace, hascode)");
//                                db.setVersion(1);
                            db.execSQL("CREATE TABLE IF NOT EXISTS apitrace (id INTEGER primary key autoincrement, stacktrace TEXT NOT NULL, hascode INTEGER  NOT NULL, mtdsig TEXT NOT NULL, times INTEGER NOT NULL, topaty TEXT)");
                            db.setTransactionSuccessful();
                        } finally {
                            db.endTransaction();
                        }
                    } finally {
                        mLock.writeLock().unlock();
                    }

//                    }

                    Util.log(null, Log.WARN, "Running VACUUM");
                    mLock.writeLock().lock();
                    try {
                        db.execSQL("VACUUM");
                    } catch (Throwable ex) {
                        Util.bug(null, ex);
                    } finally {
                        mLock.writeLock().unlock();
                    }

                    Util.log(null, Log.WARN, "Database version=" + db.getVersion());
                    mDb = db;
                } catch (Throwable ex) {
                    mDb = null; // retry
                    Util.bug(null, ex);
                    try {
                        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(
                                "/cache/sapiguard.log", true));
                        outputStreamWriter.write(ex.toString());
                        outputStreamWriter.write("\n");
                        outputStreamWriter.write(Log.getStackTraceString(ex));
                        outputStreamWriter.write("\n");
                        outputStreamWriter.close();
                    } catch (Throwable exex) {
                        Util.bug(null, exex);
                    }
                }

            return mDb;
        }
    }

    @Override
    public int getScreenShotId(String stacktrace, String mtdSig, String topAty) throws RemoteException {
        // Get database reference
        SQLiteDatabase db = getDb();
        if (db == null)
            return 0;
        Integer hashcode = stacktrace.hashCode();
        Integer timesOfTrace = 0;
        mLock.writeLock().lock();
        try {
            db.beginTransaction();
            try {

                Cursor cursor = db.rawQuery("select * from apitrace where hascode=? AND mtdsig=?", new String[]{hashcode.toString(), mtdSig});

                ContentValues values = new ContentValues();
                values.put("stacktrace", stacktrace);
                values.put("hascode", hashcode);
                values.put("mtdsig", mtdSig);
                values.put("topaty", topAty);
                if (cursor.moveToFirst()) {
                    // Create record
                    Integer id = cursor.getInt(cursor.getColumnIndex("id"));
                    timesOfTrace = cursor.getInt(cursor.getColumnIndex("times")) + 1;
                    values.put("times", timesOfTrace);
                    db.updateWithOnConflict(cTableAPITrace, values, "id=?", new String[]{id.toString()}, SQLiteDatabase.CONFLICT_IGNORE);
                } else {
                    values.put("times", 1);
                    timesOfTrace = 1;
                    // Insert/update record
                    db.insertWithOnConflict(cTableAPITrace, null, values,
                            SQLiteDatabase.CONFLICT_REPLACE);
                }

                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
        } finally {
            SimpleDateFormat formatter = new SimpleDateFormat("MMddHHmmss");
            Date date = new Date();
            final String record = String.format("%s ++ %s ++ %d ++ %d ++ %s",topAty, mtdSig,stacktrace.hashCode(),timesOfTrace,formatter.format(date));
            Log.d("jie-record", record);
            mLock.writeLock().unlock();
        }

        return timesOfTrace;

    }

    @Override
    public void checkStackTraceofHookApi(String stacktrace, String mtdSig) throws RemoteException {
        String topAty = getTopActivityName();

        int screenShotId = getScreenShotId(stacktrace, mtdSig, topAty);
        SimpleDateFormat formatter = new SimpleDateFormat("MMddHHmmss");
        Date date = new Date();
//        final String screenShotName = String.format("%s_%d_%d_%s",mtdSig,stacktrace.hashCode(),screenShotId,formatter.format(date));
//
//        Thread screenshotT = new Thread(new Runnable() {
//            @Override
//            public void run() {
//                getScreenShot(screenShotName);
//            }
//        });
//        screenshotT.start();
        if (topAty==null){
            topAty="";
        }
        final String record = String.format("%s ++ %s ++ %d ++ %d ++ %s",topAty, mtdSig,stacktrace.hashCode(),screenShotId,formatter.format(date));
//        Log.d("jie-record", record);

        Thread recordT = new Thread(new Runnable() {
            @Override
            public void run() {
                logRecord(record);
            }
        });
        recordT.start();

    }

    private void logRecord(String record) {

        String recordFile = Environment.getDataDirectory() + File.separator + "system" + File.separator + "sapiguard" + File.separator + "record.txt";
        try {
            OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(
                    recordFile, true));
            outputStreamWriter.write(record);
            outputStreamWriter.write("\n");
            outputStreamWriter.close();
        } catch (Throwable exex) {
            Util.bug(null, exex);
        }
    }

    private void getScreenShot(String shotName) {

        Activity topAty = PrivacyManager.getTopActivity();
        String screenshotName = Environment.getExternalStorageDirectory() + File.separator +"sapiguardSS" + File.separator  + shotName + ".png";

        if (topAty!=null){
            Log.d("jie-ssbyview", shotName);
            try {
                // create bitmap screen capture
                View v1 = topAty.getWindow().getDecorView().getRootView();
                v1.setDrawingCacheEnabled(true);
                Bitmap bitmap = Bitmap.createBitmap(v1.getDrawingCache());
                v1.setDrawingCacheEnabled(false);
                File imageFile = new File(screenshotName);

                FileOutputStream outputStream = new FileOutputStream(imageFile);
                int quality = 100;
                bitmap.compress(Bitmap.CompressFormat.PNG, quality, outputStream);
                outputStream.flush();
                outputStream.close();

            } catch (Throwable e) {
                Log.e("jie-viewerror",e.getMessage());
                getScreenShotBySysCmd(screenshotName);
            }

        }else{
            getScreenShotBySysCmd(screenshotName);
        }
    }




    private void getScreenShotBySysCmd(String screenshotName){
        Log.d("jie-ssbysyscmd", screenshotName);

        try {
            java.lang.Process sh = Runtime.getRuntime().exec("su", null, null);
            OutputStream os = sh.getOutputStream();
            os.write(("/system/bin/screencap -p " + screenshotName).getBytes("ASCII"));
            os.flush();
            os.close();
            sh.waitFor();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    @Override
    public void clear() throws RemoteException {
        SQLiteDatabase db = getDb();
        if (db == null)
            return;
        mLock.writeLock().lock();
        try {
            db.beginTransaction();
            try {
                db.execSQL("DELETE FROM " + cTableAPITrace);
//                db.execSQL("DROP TABLE IF EXISTS "+ cTableAPITrace);
                Util.log(null, Log.WARN, "Database cleared");
                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
        } finally {
            mLock.writeLock().unlock();
        }
    }

    @Override
    public List<SensitiveApiRecord> readDBContent() throws RemoteException {
        Log.d("jie-copy", "copyDBtoExternalStorage");
        Log.d("jie-uid", String.valueOf(Binder.getCallingUid()));

        List<SensitiveApiRecord> arrayList = new ArrayList<>();
        // Get database reference
        SQLiteDatabase db = getDb();
        if (db == null)
            return arrayList;
        mLock.writeLock().lock();
        try {
            db.beginTransaction();
            try {

                Cursor cursor = db.rawQuery("select * from apitrace", null);
                while (cursor.moveToNext()) {
                    Integer id = cursor.getInt(cursor.getColumnIndex("id"));
                    String stacktrace = cursor.getString(cursor.getColumnIndex("stacktrace"));

                    String mtdsig = cursor.getString(cursor.getColumnIndex("mtdsig"));
                    Integer hascode = cursor.getInt(cursor.getColumnIndex("hascode"));

                    Integer times = cursor.getInt(cursor.getColumnIndex("times"));
                    String topAty = cursor.getString(cursor.getColumnIndex("topaty"));

                    SensitiveApiRecord sensitiveApiRecord = new SensitiveApiRecord(id,stacktrace,hascode,mtdsig,times, topAty);
                    arrayList.add(sensitiveApiRecord);
                }




                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
            }
        } finally {
            mLock.writeLock().unlock();
        }


        Log.d("jie-copy", String.valueOf(arrayList.size()));

        return arrayList;
    }

    @Override
    public List<String> readRecordConent() throws RemoteException {
        Log.d("jie-copy", "copyRecordtoExternalStorage");

        List<String> arrayList = new ArrayList<>();
        String recordFile = Environment.getDataDirectory() + File.separator + "system" + File.separator + "sapiguard" + File.separator + "record.txt";

        try (BufferedReader br = new BufferedReader(new FileReader(recordFile))) {
            String line;
            while ((line = br.readLine()) != null) {
                arrayList.add(line);
            }
        } catch (Exception ex) {
            Util.bug(null, ex);
        }
        return arrayList;
    }
}
