package com.android.settings.applications;

import android.app.Activity;
import android.app.ActivityManager;
import android.app.ApplicationErrorReport;
import android.app.Dialog;
import android.app.PendingIntent;
import android.app.settings.SettingsEnums;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ProviderInfo;
import android.content.pm.ServiceInfo;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Debug;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;

import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.core.InstrumentedFragment;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settingslib.utils.ThreadUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;

public class RunningServiceDetails extends InstrumentedFragment
        implements RunningState.OnRefreshUiListener {
    static final String TAG = "RunningServicesDetails";

    static final String KEY_UID = "uid";
    static final String KEY_USER_ID = "user_id";
    static final String KEY_PROCESS = "process";
    static final String KEY_BACKGROUND = "background";

    static final int DIALOG_CONFIRM_STOP = 1;

    ActivityManager mAm;
    LayoutInflater mInflater;

    RunningState mState;
    boolean mHaveData;

    int mUid;
    int mUserId;
    String mProcessName;
    boolean mShowBackground;

    RunningState.MergedItem mMergedItem;

    View mRootView;
    ViewGroup mAllDetails;
    ViewGroup mSnippet;
    RunningProcessesView.ActiveItem mSnippetActiveItem;
    RunningProcessesView.ViewHolder mSnippetViewHolder;

    int mNumServices, mNumProcesses;
    
    TextView mServicesHeader;
    TextView mProcessesHeader;
    final ArrayList<ActiveDetail> mActiveDetails = new ArrayList<ActiveDetail>();
    
    class ActiveDetail implements View.OnClickListener {
        View mRootView;
        Button mStopButton;
        Button mReportButton;
        RunningState.ServiceItem mServiceItem;
        RunningProcessesView.ActiveItem mActiveItem;
        RunningProcessesView.ViewHolder mViewHolder;
        PendingIntent mManageIntent;
        ComponentName mInstaller;

        void stopActiveService(boolean confirmed) {
            RunningState.ServiceItem si = mServiceItem;
            if (!confirmed) {
                if ((si.mServiceInfo.applicationInfo.flags&ApplicationInfo.FLAG_SYSTEM) != 0) {
                    showConfirmStopDialog(si.mRunningService.service);
                    return;
                }
            }
            getActivity().stopService(new Intent().setComponent(si.mRunningService.service));
            if (mMergedItem == null) {
                // If this is gone, we are gone.
                mState.updateNow();
                finish();
            } else if (!mShowBackground && mMergedItem.mServices.size() <= 1) {
                // If there was only one service, we are finishing it,
                // so no reason for the UI to stick around.
                mState.updateNow();
                finish();
            } else {
                mState.updateNow();
            }
        }
        
        public void onClick(View v) {
            if (v == mReportButton) {
                ApplicationErrorReport report = new ApplicationErrorReport();
                report.type = ApplicationErrorReport.TYPE_RUNNING_SERVICE;
                report.packageName = mServiceItem.mServiceInfo.packageName;
                report.installerPackageName = mInstaller.getPackageName();
                report.processName = mServiceItem.mRunningService.process;
                report.time = System.currentTimeMillis();
                report.systemApp = (mServiceItem.mServiceInfo.applicationInfo.flags
                        & ApplicationInfo.FLAG_SYSTEM) != 0;
                ApplicationErrorReport.RunningServiceInfo info
                        = new ApplicationErrorReport.RunningServiceInfo();
                if (mActiveItem.mFirstRunTime >= 0) {
                    info.durationMillis = SystemClock.elapsedRealtime()-mActiveItem.mFirstRunTime;
                } else {
                    info.durationMillis = -1;
                }
                ComponentName comp = new ComponentName(mServiceItem.mServiceInfo.packageName,
                        mServiceItem.mServiceInfo.name);
                File filename = getActivity().getFileStreamPath("service_dump.txt");
                FileOutputStream output = null;
                try {
                    output = new FileOutputStream(filename);
                    Debug.dumpService("activity", output.getFD(),
                            new String[] { "-a", "service", comp.flattenToString() });
                } catch (IOException e) {
                    Log.w(TAG, "Can't dump service: " + comp, e);
                } finally {
                    if (output != null) try { output.close(); } catch (IOException e) {}
                }
                FileInputStream input = null;
                try {
                    input = new FileInputStream(filename);
                    byte[] buffer = new byte[(int) filename.length()];
                    input.read(buffer);
                    info.serviceDetails = new String(buffer);
                } catch (IOException e) {
                    Log.w(TAG, "Can't read service dump: " + comp, e);
                } finally {
                    if (input != null) try { input.close(); } catch (IOException e) {}
                }
                filename.delete();
                Log.i(TAG, "Details: " + info.serviceDetails);
                report.runningServiceInfo = info;
                Intent result = new Intent(Intent.ACTION_APP_ERROR);
                result.setComponent(mInstaller);
                result.putExtra(Intent.EXTRA_BUG_REPORT, report);
                result.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity(result);
                return;
            }

            if (mManageIntent != null) {
                try {
                    getActivity().startIntentSender(mManageIntent.getIntentSender(), null,
                            Intent.FLAG_ACTIVITY_NEW_TASK
                                    | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET,
                            Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET, 0);
                } catch (IntentSender.SendIntentException e) {
                    Log.w(TAG, e);
                } catch (IllegalArgumentException e) {
                    Log.w(TAG, e);
                } catch (ActivityNotFoundException e) {
                    Log.w(TAG, e);
                }
            } else if (mServiceItem != null) {
                stopActiveService(false);
            } else if (mActiveItem.mItem.mBackground) {
                // Background process.  Just kill it.
                mAm.killBackgroundProcesses(mActiveItem.mItem.mPackageInfo.packageName);
                finish();
            } else {
                // Heavy-weight process.  We'll do a force-stop on it.
                mAm.forceStopPackage(mActiveItem.mItem.mPackageInfo.packageName);
                finish();
            }
        }
    }
    
    StringBuilder mBuilder = new StringBuilder(128);
    
    boolean findMergedItem() {
        RunningState.MergedItem item = null;
        ArrayList<RunningState.MergedItem> newItems = mShowBackground
                ? mState.getCurrentBackgroundItems() : mState.getCurrentMergedItems();
        if (newItems != null) {
            for (int i=0; i<newItems.size(); i++) {
                RunningState.MergedItem mi = newItems.get(i);
                if (mi.mUserId != mUserId) {
                    continue;
                }
                if (mUid >= 0 && mi.mProcess != null && mi.mProcess.mUid != mUid) {
                    continue;
                }
                if (mProcessName == null || (mi.mProcess != null
                        && mProcessName.equals(mi.mProcess.mProcessName))) {
                    item = mi;
                    break;
                }
            }
        }

        if (mMergedItem != item) {
            mMergedItem = item;
            return true;
        }
        return false;
    }

    void addServicesHeader() {
        if (mNumServices == 0) {
            mServicesHeader = (TextView) mInflater.inflate(R.layout.preference_category,
                    mAllDetails, false);
            mServicesHeader.setText(R.string.runningservicedetails_services_title);
            mAllDetails.addView(mServicesHeader);
        }
        mNumServices++;
    }

    void addProcessesHeader() {
        if (mNumProcesses == 0) {
            mProcessesHeader = (TextView) mInflater.inflate(R.layout.preference_category,
                    mAllDetails, false);
            mProcessesHeader.setText(R.string.runningservicedetails_processes_title);
            mAllDetails.addView(mProcessesHeader);
        }
        mNumProcesses++;
    }

    void addServiceDetailsView(RunningState.ServiceItem si, RunningState.MergedItem mi,
            boolean isService, boolean inclDetails) {
        if (isService) {
            addServicesHeader();
        } else if (mi.mUserId != UserHandle.myUserId()) {
            // This is being called for another user, and is not a service...
            // That is, it is a background processes, being added for the
            // details of a user.  In this case we want a header for processes,
            // since the top subject line is for the user.
            addProcessesHeader();
        }

        RunningState.BaseItem bi = si != null ? si : mi;
        
        ActiveDetail detail = new ActiveDetail();
        View root = mInflater.inflate(R.layout.running_service_details_service,
                mAllDetails, false);
        mAllDetails.addView(root);
        detail.mRootView = root;
        detail.mServiceItem = si;
        detail.mViewHolder = new RunningProcessesView.ViewHolder(root);
        detail.mActiveItem = detail.mViewHolder.bind(mState, bi, mBuilder);

        if (!inclDetails) {
            root.findViewById(R.id.service).setVisibility(View.GONE);
        }

        if (si != null && si.mRunningService.clientLabel != 0) {
            detail.mManageIntent = mAm.getRunningServiceControlPanel(
                    si.mRunningService.service);
        }
        
        TextView description = (TextView)root.findViewById(R.id.comp_description);
        detail.mStopButton = (Button)root.findViewById(R.id.left_button);
        detail.mReportButton = (Button)root.findViewById(R.id.right_button);

        if (isService && mi.mUserId != UserHandle.myUserId()) {
            // For services from other users, we don't show any description or
            // controls, because the current user can not perform
            // actions on them.
            description.setVisibility(View.GONE);
            root.findViewById(R.id.control_buttons_panel).setVisibility(View.GONE);
        } else {
            if (si != null && si.mServiceInfo.descriptionRes != 0) {
                description.setText(getActivity().getPackageManager().getText(
                        si.mServiceInfo.packageName, si.mServiceInfo.descriptionRes,
                        si.mServiceInfo.applicationInfo));
            } else {
                if (mi.mBackground) {
                    description.setText(R.string.background_process_stop_description);
                } else if (detail.mManageIntent != null) {
                    try {
                        Resources clientr = getActivity().getPackageManager().getResourcesForApplication(
                                si.mRunningService.clientPackage);
                        String label = clientr.getString(si.mRunningService.clientLabel);
                        description.setText(getActivity().getString(R.string.service_manage_description,
                                label));
                    } catch (PackageManager.NameNotFoundException e) {
                    }
                } else {
                    description.setText(getActivity().getText(si != null
                            ? R.string.service_stop_description
                            : R.string.heavy_weight_stop_description));
                }
            }

            detail.mStopButton.setOnClickListener(detail);
            detail.mStopButton.setText(getActivity().getText(detail.mManageIntent != null
                    ? R.string.service_manage : R.string.service_stop));
            detail.mReportButton.setOnClickListener(detail);
            detail.mReportButton.setText(com.android.internal.R.string.report);
            // check if error reporting is enabled in secure settings
            int enabled = Settings.Global.getInt(getActivity().getContentResolver(),
                    Settings.Global.SEND_ACTION_APP_ERROR, 0);
            if (enabled != 0 && si != null) {
                detail.mInstaller = ApplicationErrorReport.getErrorReportReceiver(
                        getActivity(), si.mServiceInfo.packageName,
                        si.mServiceInfo.applicationInfo.flags);
                detail.mReportButton.setEnabled(detail.mInstaller != null);
            } else {
                detail.mReportButton.setEnabled(false);
            }
        }

        mActiveDetails.add(detail);
    }

    void addProcessDetailsView(RunningState.ProcessItem pi, boolean isMain) {
        addProcessesHeader();

        ActiveDetail detail = new ActiveDetail();
        View root = mInflater.inflate(R.layout.running_service_details_process,
                mAllDetails, false);
        mAllDetails.addView(root);
        detail.mRootView = root;
        detail.mViewHolder = new RunningProcessesView.ViewHolder(root);
        detail.mActiveItem = detail.mViewHolder.bind(mState, pi, mBuilder);
        
        TextView description = (TextView)root.findViewById(R.id.comp_description);
        if (pi.mUserId != UserHandle.myUserId()) {
            // Processes for another user are all shown batched together; there is
            // no reason to have a description.
            description.setVisibility(View.GONE);
        } else if (isMain) {
            description.setText(R.string.main_running_process_description);
        } else {
            int textid = 0;
            CharSequence label = null;
            ActivityManager.RunningAppProcessInfo rpi = pi.mRunningProcessInfo;
            final ComponentName comp = rpi.importanceReasonComponent;
            //Log.i(TAG, "Secondary proc: code=" + rpi.importanceReasonCode
            //        + " pid=" + rpi.importanceReasonPid + " comp=" + comp);
            switch (rpi.importanceReasonCode) {
                case ActivityManager.RunningAppProcessInfo.REASON_PROVIDER_IN_USE:
                    textid = R.string.process_provider_in_use_description;
                    if (rpi.importanceReasonComponent != null) {
                        try {
                            ProviderInfo prov = getActivity().getPackageManager().getProviderInfo(
                                    rpi.importanceReasonComponent, 0);
                            label = RunningState.makeLabel(getActivity().getPackageManager(),
                                    prov.name, prov);
                        } catch (NameNotFoundException e) {
                        }
                    }
                    break;
                case ActivityManager.RunningAppProcessInfo.REASON_SERVICE_IN_USE:
                    textid = R.string.process_service_in_use_description;
                    if (rpi.importanceReasonComponent != null) {
                        try {
                            ServiceInfo serv = getActivity().getPackageManager().getServiceInfo(
                                    rpi.importanceReasonComponent, 0);
                            label = RunningState.makeLabel(getActivity().getPackageManager(),
                                    serv.name, serv);
                        } catch (NameNotFoundException e) {
                        }
                    }
                    break;
            }
            if (textid != 0 && label != null) {
                description.setText(getActivity().getString(textid, label));
            }
        }
        
        mActiveDetails.add(detail);
    }

    void addDetailsViews(RunningState.MergedItem item, boolean inclServices,
            boolean inclProcesses) {
        if (item != null) {
            if (inclServices) {
                for (int i=0; i<item.mServices.size(); i++) {
                    addServiceDetailsView(item.mServices.get(i), item, true, true);
                }
            }

            if (inclProcesses) {
                if (item.mServices.size() <= 0) {
                    // This item does not have any services, so it must be
                    // another interesting process...  we will put a fake service
                    // entry for it, to allow the user to "stop" it.
                    addServiceDetailsView(null, item, false, item.mUserId != UserHandle.myUserId());
                } else {
                    // This screen is actually showing services, so also show
                    // the process details.
                    for (int i=-1; i<item.mOtherProcesses.size(); i++) {
                        RunningState.ProcessItem pi = i < 0 ? item.mProcess
                                : item.mOtherProcesses.get(i);
                        if (pi != null && pi.mPid <= 0) {
                            continue;
                        }
    
                        addProcessDetailsView(pi, i < 0);
                    }
                }
            }
        }
    }

    void addDetailViews() {
        for (int i=mActiveDetails.size()-1; i>=0; i--) {
            mAllDetails.removeView(mActiveDetails.get(i).mRootView);
        }
        mActiveDetails.clear();
        
        if (mServicesHeader != null) {
            mAllDetails.removeView(mServicesHeader);
            mServicesHeader = null;
        }
        
        if (mProcessesHeader != null) {
            mAllDetails.removeView(mProcessesHeader);
            mProcessesHeader = null;
        }

        mNumServices = mNumProcesses = 0;

        if (mMergedItem != null) {
            if (mMergedItem.mUser != null) {
                ArrayList<RunningState.MergedItem> items;
                if (mShowBackground) {
                    items = new ArrayList<RunningState.MergedItem>(mMergedItem.mChildren);
                    Collections.sort(items, mState.mBackgroundComparator);
                } else {
                    items = mMergedItem.mChildren;
                }
                for (int i=0; i<items.size(); i++) {
                    addDetailsViews(items.get(i), true, false);
                }
                for (int i=0; i<items.size(); i++) {
                    addDetailsViews(items.get(i), false, true);
                }
            } else {
                addDetailsViews(mMergedItem, true, true);
            }
        }
    }
    
    void refreshUi(boolean dataChanged) {
        if (findMergedItem()) {
            dataChanged = true;
        }
        if (dataChanged) {
            if (mMergedItem != null) {
                mSnippetActiveItem = mSnippetViewHolder.bind(mState,
                        mMergedItem, mBuilder);
            } else if (mSnippetActiveItem != null) {
                // Clear whatever is currently being shown.
                mSnippetActiveItem.mHolder.size.setText("");
                mSnippetActiveItem.mHolder.uptime.setText("");
                mSnippetActiveItem.mHolder.description.setText(R.string.no_services);
            } else {
                // No merged item, never had one.  Nothing to do.
                finish();
                return;
            }
            addDetailViews();
        }
    }

    private void finish() {
        ThreadUtils.postOnMainThread(() -> {
            final Activity a = getActivity();
            if (a != null) {
                a.onBackPressed();
            }
        });
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
        mUid = getArguments().getInt(KEY_UID, -1);
        mUserId = getArguments().getInt(KEY_USER_ID, 0);
        mProcessName = getArguments().getString(KEY_PROCESS, null);
        mShowBackground = getArguments().getBoolean(KEY_BACKGROUND, false);

        mAm = (ActivityManager) getActivity().getSystemService(Context.ACTIVITY_SERVICE);
        mInflater = (LayoutInflater) getActivity().getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);

        mState = RunningState.getInstance(getActivity());
    }
    
    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final View view = inflater.inflate(R.layout.running_service_details, container, false);
        Utils.prepareCustomPreferencesList(container, view, view, false);

        mRootView = view;
        mAllDetails = (ViewGroup)view.findViewById(R.id.all_details);
        mSnippet = (ViewGroup)view.findViewById(R.id.snippet);
        mSnippetViewHolder = new RunningProcessesView.ViewHolder(mSnippet);
        
        // We want to retrieve the data right now, so any active managed
        // dialog that gets created can find it.
        ensureData();
        
        return view;
    }

    @Override
    public void onPause() {
        super.onPause();
        mHaveData = false;
        mState.pause();
    }

    @Override
    public int getMetricsCategory() {
        return SettingsEnums.RUNNING_SERVICE_DETAILS;
    }

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

    ActiveDetail activeDetailForService(ComponentName comp) {
        for (int i=0; i<mActiveDetails.size(); i++) {
            ActiveDetail ad = mActiveDetails.get(i);
            if (ad.mServiceItem != null && ad.mServiceItem.mRunningService != null
                    && comp.equals(ad.mServiceItem.mRunningService.service)) {
                return ad;
            }
        }
        return null;
    }

    private void showConfirmStopDialog(ComponentName comp) {
        DialogFragment newFragment = MyAlertDialogFragment.newConfirmStop(
                DIALOG_CONFIRM_STOP, comp);
        newFragment.setTargetFragment(this, 0);
        newFragment.show(getFragmentManager(), "confirmstop");
    }

    public static class MyAlertDialogFragment extends InstrumentedDialogFragment {

        public static MyAlertDialogFragment newConfirmStop(int id, ComponentName comp) {
            MyAlertDialogFragment frag = new MyAlertDialogFragment();
            Bundle args = new Bundle();
            args.putInt("id", id);
            args.putParcelable("comp", comp);
            frag.setArguments(args);
            return frag;
        }

        RunningServiceDetails getOwner() {
            return (RunningServiceDetails)getTargetFragment();
        }

        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            int id = getArguments().getInt("id");
            switch (id) {
                case DIALOG_CONFIRM_STOP: {
                    final ComponentName comp = (ComponentName)getArguments().getParcelable("comp");
                    if (getOwner().activeDetailForService(comp) == null) {
                        return null;
                    }
                    
                    return new AlertDialog.Builder(getActivity())
                            .setTitle(getActivity().getString(R.string.runningservicedetails_stop_dlg_title))
                            .setMessage(getActivity().getString(R.string.runningservicedetails_stop_dlg_text))
                            .setPositiveButton(R.string.dlg_ok,
                                    new DialogInterface.OnClickListener() {
                                public void onClick(DialogInterface dialog, int which) {
                                    ActiveDetail ad = getOwner().activeDetailForService(comp);
                                    if (ad != null) {
                                        ad.stopActiveService(true);
                                    }
                                }
                            })
                            .setNegativeButton(R.string.dlg_cancel, null)
                            .create();
                }
            }
            throw new IllegalArgumentException("unknown id " + id);
        }

        @Override
        public int getMetricsCategory() {
            return SettingsEnums.DIALOG_RUNNIGN_SERVICE;
        }
    }

    void ensureData() {
        if (!mHaveData) {
            mHaveData = true;
            mState.resume(this);

            // We want to go away if the service being shown no longer exists,
            // so we need to ensure we have done the initial data retrieval before
            // showing our ui.
            mState.waitForData();

            // And since we know we have the data, let's show the UI right away
            // to avoid flicker.
            refreshUi(true);
        }
    }
    
    void updateTimes() {
        if (mSnippetActiveItem != null) {
            mSnippetActiveItem.updateTime(getActivity(), mBuilder);
        }
        for (int i=0; i<mActiveDetails.size(); i++) {
            mActiveDetails.get(i).mActiveItem.updateTime(getActivity(), mBuilder);
        }
    }

    @Override
    public void onRefreshUi(int what) {
        if (getActivity() == null) return;
        switch (what) {
            case REFRESH_TIME:
                updateTimes();
                break;
            case REFRESH_DATA:
                refreshUi(false);
                updateTimes();
                break;
            case REFRESH_STRUCTURE:
                refreshUi(true);
                updateTimes();
                break;
        }
    }
}
