/*
 * Copyright (C) 2013 Koushik Dutta (@koush)
 *
 * 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.koushikdutta.superuser;

import java.io.DataInputStream;
import java.io.File;
import java.util.HashMap;

import junit.framework.Assert;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.net.LocalSocketAddress.Namespace;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;

import com.koushikdutta.superuser.db.SuDatabaseHelper;
import com.koushikdutta.superuser.db.UidPolicy;
import com.koushikdutta.superuser.util.Settings;
import com.koushikdutta.superuser.util.SuHelper;

@SuppressLint("ValidFragment")
public class MultitaskSuRequestActivity extends FragmentActivity {
    private static final String LOGTAG = "Superuser";
    int mCallerUid;
    int mDesiredUid;
    String mDesiredCmd;
    int mPid;
    
    Spinner mSpinner;
    
    Handler mHandler = new Handler();
    
    int mTimeLeft = 3;
    
    Button mAllow;
    Button mDeny;

    boolean mHandled;
    
    public int getGracePeriod() {
        return 10;
    }
    
    int getUntil() {
        int until = -1;
        if (mSpinner.isShown()) {
            int pos = mSpinner.getSelectedItemPosition();
            int id = mSpinnerIds[pos];
            if (id == R.string.remember_for) {
                until = (int)(System.currentTimeMillis() / 1000) + getGracePeriod() * 60;
            }
            else if (id == R.string.remember_forever) {
                until = 0;
            }
        }
        else if (mRemember.isShown()) {
            if (mRemember.getCheckedRadioButtonId() == R.id.remember_for) {
                until = (int)(System.currentTimeMillis() / 1000) + getGracePeriod() * 60;
            }
            else if (mRemember.getCheckedRadioButtonId() == R.id.remember_forever) {
                until = 0;
            }
        }
        return until;
    }
    
    void handleAction(boolean action, Integer until) {
        Assert.assertTrue(!mHandled);
        mHandled = true;
        try {
            mSocket.getOutputStream().write((action ? "socket:ALLOW" : "socket:DENY").getBytes());
        }
        catch (Exception ex) {
        }
        try {
            if (until == null) {
                until = getUntil();
            }
            // got a policy? let's set it.
            if (until != -1) {
                UidPolicy policy = new UidPolicy();
                policy.policy = action ? UidPolicy.ALLOW : UidPolicy.DENY;
                policy.uid = mCallerUid;
                // for now just approve all commands, since per command approval is stupid
//                policy.command = mDesiredCmd;
                policy.command = null;
                policy.until = until;
                policy.desiredUid = mDesiredUid;
                SuDatabaseHelper.setPolicy(this, policy);
            }
            // TODO: logging? or should su binary handle that via broadcast?
            // Probably the latter, so it is consolidated and from the system of record.
        }
        catch (Exception ex) {
        }
        finish();
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (!mHandled)
            handleAction(false, -1);
        try {
            if (mSocket != null)
                mSocket.close();
        }
        catch (Exception ex) {
        }
        new File(mSocketPath).delete();
    }

    public static final String PERMISSION = "android.permission.ACCESS_SUPERUSER";
    
    boolean mRequestReady;
    void requestReady() {
        findViewById(R.id.incoming).setVisibility(View.GONE);
        findViewById(R.id.ready).setVisibility(View.VISIBLE);
        
        final View packageInfo = findViewById(R.id.packageinfo);
        final PackageManager pm = getPackageManager();
        String[] pkgs = pm.getPackagesForUid(mCallerUid);
        TextView unknown = (TextView)findViewById(R.id.unknown);
        unknown.setText(getString(R.string.unknown_uid, mCallerUid));

        final View appInfo = findViewById(R.id.app_info);
        appInfo.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (packageInfo.getVisibility() == View.GONE) {
                    appInfo.setVisibility(View.GONE);
                    packageInfo.setVisibility(View.VISIBLE);
                }
            }
        });
        
        packageInfo.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (appInfo.getVisibility() == View.GONE) {
                    appInfo.setVisibility(View.VISIBLE);
                    packageInfo.setVisibility(View.GONE);
                }
            }
        });
        
        ((TextView)findViewById(R.id.uid_header)).setText(Integer.toString(mDesiredUid));
        ((TextView)findViewById(R.id.command_header)).setText(mDesiredCmd);

        boolean superuserDeclared = false;
        boolean granted = false;
        if (pkgs != null && pkgs.length > 0) {
            for (String pkg: pkgs) {
                try {
                    PackageInfo pi = pm.getPackageInfo(pkg, PackageManager.GET_PERMISSIONS);
                    ((TextView)findViewById(R.id.request)).setText(getString(R.string.application_request, pi.applicationInfo.loadLabel(pm)));
                    ImageView icon = (ImageView)packageInfo.findViewById(R.id.image);
                    icon.setImageDrawable(pi.applicationInfo.loadIcon(pm));
                    ((TextView)packageInfo.findViewById(R.id.title)).setText(pi.applicationInfo.loadLabel(pm));
                    
                    ((TextView)findViewById(R.id.app_header)).setText(pi.applicationInfo.loadLabel(pm));
                    ((TextView)findViewById(R.id.package_header)).setText(pi.packageName);
                    
                    if (pi.requestedPermissions != null) {
                        for (String perm: pi.requestedPermissions) {
                            if (PERMISSION.equals(perm)) {
                                superuserDeclared = true;
                                break;
                            }
                        }
                    }
                    
                    granted |= checkPermission(PERMISSION, mPid, mCallerUid) == PackageManager.PERMISSION_GRANTED;
                    
                    // could display them all, but screw it...
                    // maybe a better ux for this later
                    break;
                }
                catch (Exception ex) {
                }
            }
            findViewById(R.id.unknown).setVisibility(View.GONE);
        }
        
        if (!superuserDeclared) {
            findViewById(R.id.developer_warning).setVisibility(View.VISIBLE);
        }
        
        // handle automatic responses
        // these will be considered permanent user policies
        // even though they are automatic.
        // this is so future su requests dont invoke ui

        // handle declared permission
        if (Settings.getRequirePermission(MultitaskSuRequestActivity.this) && !superuserDeclared) {
            Log.i(LOGTAG, "Automatically denying due to missing permission");
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (!mHandled)
                        handleAction(false, 0);
                }
            });
            return;
        }

        // automatic response
        switch (Settings.getAutomaticResponse(MultitaskSuRequestActivity.this)) {
        case Settings.AUTOMATIC_RESPONSE_ALLOW:
//            // automatic response and pin can not be used together
//            if (Settings.isPinProtected(MultitaskSuRequestActivity.this))
//                break;
            // check if the permission must be granted 
            if (Settings.getRequirePermission(MultitaskSuRequestActivity.this) && !granted)
                break;
            Log.i(LOGTAG, "Automatically allowing due to user preference");
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (!mHandled)
                        handleAction(true, 0);
                }
            });
            return;
        case Settings.AUTOMATIC_RESPONSE_DENY:
            Log.i(LOGTAG, "Automatically denying due to user preference");
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    if (!mHandled)
                        handleAction(false, 0);
                }
            });
            return;
        }
        
        new Runnable() {
            public void run() {
                mAllow.setText(getString(R.string.allow) + " (" + mTimeLeft + ")");
                if (mTimeLeft-- <= 0) {
                    mAllow.setText(getString(R.string.allow));
                    if (!mHandled)
                        mAllow.setEnabled(true);
                    return;
                }
                mHandler.postDelayed(this, 1000);
            };
        }.run();
    }

    private final static int SU_PROTOCOL_PARAM_MAX = 20;
    private final static int SU_PROTOCOL_NAME_MAX = 20;
    private final static int SU_PROTOCOL_VALUE_MAX_DEFAULT = 256;
    private final static HashMap<String, Integer> SU_PROTOCOL_VALUE_MAX = new HashMap<String, Integer>() {
        {
            put("command", 2048);
        }
    };
    
    private static int getValueMax(String name) {
        Integer max = SU_PROTOCOL_VALUE_MAX.get(name);
        if (max == null)
            return SU_PROTOCOL_VALUE_MAX_DEFAULT;
        return max;
    }
    
    void manageSocket() {
        new Thread() {
            @Override
            public void run() {
                try {
                    mSocket = new LocalSocket();
                    mSocket.connect(new LocalSocketAddress(mSocketPath, Namespace.FILESYSTEM));

                    DataInputStream is = new DataInputStream(mSocket.getInputStream());
                    
                    ContentValues payload = new ContentValues();


                    for (int i = 0; i < SU_PROTOCOL_PARAM_MAX; i++) {
                        int nameLen = is.readInt();
                        if (nameLen > SU_PROTOCOL_NAME_MAX)
                            throw new IllegalArgumentException("name length too long: " + nameLen);
                        byte[] nameBytes = new byte[nameLen];
                        is.readFully(nameBytes);
                        String name = new String(nameBytes);
                        int dataLen = is.readInt();
                        if (dataLen > getValueMax(name))
                            throw new IllegalArgumentException(name + " data length too long: " + dataLen);
                        byte[] dataBytes = new byte[dataLen];
                        is.readFully(dataBytes);
                        String data = new String(dataBytes);
                        payload.put(name, data);
//                        Log.i(LOGTAG, name);
//                        Log.i(LOGTAG, data);
                        if ("eof".equals(name))
                            break;
                    }
                    
                    int protocolVersion = payload.getAsInteger("version");
                    mCallerUid = payload.getAsInteger("from.uid");
                    mDesiredUid = payload.getAsByte("to.uid");
                    mDesiredCmd = payload.getAsString("command");
                    String calledBin = payload.getAsString("from.bin");
                    mPid = payload.getAsInteger("pid");
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mRequestReady = true;
                            requestReady();
                        }
                    });                    

                    if ("com.koushikdutta.superuser".equals(getPackageName())) {
                        if (!SuHelper.CURRENT_VERSION.equals(payload.getAsString("binary.version")))
                            SuCheckerReceiver.doNotification(MultitaskSuRequestActivity.this);
                    }
                }
                catch (Exception ex) {
                    Log.i(LOGTAG, ex.getMessage(), ex);
                    try {
                        mSocket.close();
                    }
                    catch (Exception e) {
                    }
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            finish();
                        }
                    });
                }
            }
        }.start();
    }
    

    RadioGroup mRemember;
    
    LocalSocket mSocket;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Settings.applyDarkThemeSetting(this, R.style.RequestThemeDark);
        super.onCreate(savedInstanceState);

        Intent intent = getIntent();
        if (intent == null) {
            finish();
            return;
        }

        mSocketPath = intent.getStringExtra("socket");
        if (mSocketPath == null) {
            finish();
            return;
        }

        setContentView();

        manageSocket();
        
        
        // watch for the socket disappearing. that means su died.
        new Runnable() {
            public void run() {
                if (isFinishing())
                    return;
                if (!new File(mSocketPath).exists()) {
                    finish();
                    return;
                }
                
                mHandler.postDelayed(this, 1000);
            };
        }.run();
        
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if (isFinishing())
                    return;
                if (!mHandled)
                    handleAction(false, -1);
            }
        }, Settings.getRequestTimeout(this) * 1000);
    }
    
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        
        setContentView();
    }
    
    final int[] mSpinnerIds = new int[] {
            R.string.this_time_only,
            R.string.remember_for,
            R.string.remember_forever
    };
    
    void approve() {
        mAllow.setEnabled(false);
        mDeny.setEnabled(false);
        handleAction(true, null);
    }

    void deny() {
        mAllow.setEnabled(false);
        mDeny.setEnabled(false);
        handleAction(false, null);
    }

    String mSocketPath;
    ArrayAdapter<String> mSpinnerAdapter;
    void setContentView() {
        setContentView(R.layout.request);

        mSpinner = (Spinner)findViewById(R.id.remember_choices);
        mSpinner.setAdapter(mSpinnerAdapter = new ArrayAdapter<String>(this, R.layout.request_spinner_choice, R.id.request_spinner_choice));
        for (int id: mSpinnerIds) {
            mSpinnerAdapter.add(getString(id, getGracePeriod()));
        }
        
        mRemember = (RadioGroup)findViewById(R.id.remember);
        RadioButton rememberFor = (RadioButton)findViewById(R.id.remember_for);
        rememberFor.setText(getString(R.string.remember_for, getGracePeriod()));

        mAllow = (Button)findViewById(R.id.allow);
        mDeny = (Button)findViewById(R.id.deny);
        
        mAllow.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!Settings.isPinProtected(MultitaskSuRequestActivity.this)) {
                    approve();
                    return;
                }
                
                ViewGroup ready = (ViewGroup)findViewById(R.id.root);
                final int until = getUntil();
                ready.removeAllViews();
                
                PinViewHelper pin = new PinViewHelper(getLayoutInflater(), (ViewGroup)findViewById(android.R.id.content), null) {
                    @Override
                    public void onEnter(String password) {
                        super.onEnter(password);
                        if (Settings.checkPin(MultitaskSuRequestActivity.this, password)) {
                            mAllow.setEnabled(false);
                            mDeny.setEnabled(false);
                            handleAction(true, until);
                        }
                        else {
                            Toast.makeText(MultitaskSuRequestActivity.this, getString(R.string.incorrect_pin), Toast.LENGTH_SHORT).show();
                        }
                    }
                    @Override
                    public void onCancel() {
                        super.onCancel();
                        deny();
                    }
                };
                
                ready.addView(pin.getView());
            }
        });
        mDeny.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                deny();
            }
        });
        
        if (mRequestReady)
            requestReady();
    }
}
