/*
 * Copyright 2020 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.server.tracing;

import static com.android.internal.util.FrameworkStatsLog.TRACING_SERVICE_REPORT_EVENT;
import static com.android.internal.util.FrameworkStatsLog.TRACING_SERVICE_REPORT_EVENT__EVENT__TRACING_SERVICE_REPORT_BEGIN;
import static com.android.internal.util.FrameworkStatsLog.TRACING_SERVICE_REPORT_EVENT__EVENT__TRACING_SERVICE_REPORT_BIND_PERM_INCORRECT;
import static com.android.internal.util.FrameworkStatsLog.TRACING_SERVICE_REPORT_EVENT__EVENT__TRACING_SERVICE_REPORT_SVC_COMM_ERROR;
import static com.android.internal.util.FrameworkStatsLog.TRACING_SERVICE_REPORT_EVENT__EVENT__TRACING_SERVICE_REPORT_SVC_HANDOFF;
import static com.android.internal.util.FrameworkStatsLog.TRACING_SERVICE_REPORT_EVENT__EVENT__TRACING_SERVICE_REPORT_SVC_PERM_MISSING;

import android.Manifest;
import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ServiceInfo;
import android.os.Binder;
import android.os.IMessenger;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.tracing.TraceReportService;
import android.tracing.ITracingServiceProxy;
import android.tracing.TraceReportParams;
import android.util.Log;
import android.util.LruCache;
import android.util.Slog;

import com.android.internal.infra.ServiceConnector;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.SystemService;

import java.io.IOException;

/**
 * TracingServiceProxy is the system_server intermediary between the Perfetto tracing daemon and the
 * other components (e.g. system tracing app Traceur, trace reporting apps).
 *
 * Access to this service is restricted via SELinux. Normal apps do not have access.
 *
 * @hide
 */
public class TracingServiceProxy extends SystemService {
    public static final String TRACING_SERVICE_PROXY_BINDER_NAME = "tracing.proxy";
    private static final String TAG = "TracingServiceProxy";
    private static final String TRACING_APP_PACKAGE_NAME = "com.android.traceur";
    private static final String TRACING_APP_ACTIVITY = "com.android.traceur.StopTraceService";

    private static final int MAX_CACHED_REPORTER_SERVICES = 8;

    // The maximum size of the trace allowed if the option |usePipeForTesting| is set.
    // Note: this size MUST be smaller than the buffer size of the pipe (i.e. what you can
    // write to the pipe without blocking) to avoid system_server blocking on this.
    // (on Linux, the minimum value is 4K i.e. 1 minimally sized page).
    private static final int MAX_FILE_SIZE_BYTES_TO_PIPE = 1024;

    // Keep this in sync with the definitions in TraceService
    private static final String INTENT_ACTION_NOTIFY_SESSION_STOPPED =
            "com.android.traceur.NOTIFY_SESSION_STOPPED";

    private static final int REPORT_BEGIN =
            TRACING_SERVICE_REPORT_EVENT__EVENT__TRACING_SERVICE_REPORT_BEGIN;
    private static final int REPORT_SVC_HANDOFF =
            TRACING_SERVICE_REPORT_EVENT__EVENT__TRACING_SERVICE_REPORT_SVC_HANDOFF;
    private static final int REPORT_BIND_PERM_INCORRECT =
            TRACING_SERVICE_REPORT_EVENT__EVENT__TRACING_SERVICE_REPORT_BIND_PERM_INCORRECT;
    private static final int REPORT_SVC_PERM_MISSING =
            TRACING_SERVICE_REPORT_EVENT__EVENT__TRACING_SERVICE_REPORT_SVC_PERM_MISSING;
    private static final int REPORT_SVC_COMM_ERROR =
            TRACING_SERVICE_REPORT_EVENT__EVENT__TRACING_SERVICE_REPORT_SVC_COMM_ERROR;
    private static final String NOTIFY_SESSION_ENDED_SETTING = "should_notify_trace_session_ended";
    private static final int ENABLED = 1;

    private final Context mContext;
    private final PackageManager mPackageManager;
    private final LruCache<ComponentName, ServiceConnector<IMessenger>> mCachedReporterServices;
    private boolean mServicePublished = false;

    private final ITracingServiceProxy.Stub mTracingServiceProxy = new ITracingServiceProxy.Stub() {
        /**
         * Notifies system tracing app that a tracing session has ended. sessionStolen is ignored,
         * as trace sessions are no longer stolen and are always cloned instead.
         * <p>
         * Cases exist where user-flows besides Traceur's QS Tile may end long-trace sessions. In
         * these cases, a Global int will be set to flag the upcoming notifyTraceSessionEnded call
         * as purposely muted once.
         */
        @Override
        public void notifyTraceSessionEnded(boolean sessionStolen /* unused */) {
            long identity = Binder.clearCallingIdentity();
            if (Settings.Global.getInt(mContext.getContentResolver(),
                    NOTIFY_SESSION_ENDED_SETTING, ENABLED) == ENABLED) {
                TracingServiceProxy.this.notifyTraceur();
            } else {
                Settings.Global.putInt(mContext.getContentResolver(), NOTIFY_SESSION_ENDED_SETTING,
                        ENABLED);
            }
            Binder.restoreCallingIdentity(identity);
        }

        @Override
        public void reportTrace(@NonNull TraceReportParams params) {
            TracingServiceProxy.this.reportTrace(params);
        }
    };

    public TracingServiceProxy(Context context) {
        super(context);
        mContext = context;
        mPackageManager = context.getPackageManager();
        mCachedReporterServices = new LruCache<>(MAX_CACHED_REPORTER_SERVICES);
    }

    @Override
    public void onStart() {}

    @Override
    public void onUserUnlocking(@NonNull TargetUser user) {
        // We need the device storage to be unlocked before we can accept and forward
        // requests.
        if (!mServicePublished) {
            publishBinderService(TRACING_SERVICE_PROXY_BINDER_NAME, mTracingServiceProxy);
            mServicePublished = true;
        }
    }

    private void notifyTraceur() {
        final Intent intent = new Intent();

        try {
            // Validate that Traceur is a system app.
            PackageInfo info = mPackageManager.getPackageInfo(TRACING_APP_PACKAGE_NAME,
                    PackageManager.MATCH_SYSTEM_ONLY);

            intent.setClassName(info.packageName, TRACING_APP_ACTIVITY);
            intent.setAction(INTENT_ACTION_NOTIFY_SESSION_STOPPED);

            final long identity = Binder.clearCallingIdentity();
            try {
                mContext.startForegroundServiceAsUser(intent, UserHandle.SYSTEM);
            } catch (RuntimeException e) {
                Log.e(TAG, "Failed to notifyTraceSessionEnded", e);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }

        } catch (NameNotFoundException e) {
            Log.e(TAG, "Failed to locate Traceur", e);
        }
    }

    private void reportTrace(@NonNull TraceReportParams params) {
        FrameworkStatsLog.write(TRACING_SERVICE_REPORT_EVENT, REPORT_BEGIN,
                params.uuidLsb, params.uuidMsb);

        // We don't need to do any permission checks on the caller because access
        // to this service is guarded by SELinux.
        ComponentName component = new ComponentName(params.reporterPackageName,
                params.reporterClassName);
        if (!hasBindServicePermission(component)) {
            FrameworkStatsLog.write(TRACING_SERVICE_REPORT_EVENT, REPORT_BIND_PERM_INCORRECT,
                    params.uuidLsb, params.uuidMsb);
            return;
        }
        boolean hasDumpPermission = hasPermission(component, Manifest.permission.DUMP);
        boolean hasUsageStatsPermission = hasPermission(component,
                Manifest.permission.PACKAGE_USAGE_STATS);
        if (!hasDumpPermission || !hasUsageStatsPermission) {
            FrameworkStatsLog.write(TRACING_SERVICE_REPORT_EVENT, REPORT_SVC_PERM_MISSING,
                    params.uuidLsb, params.uuidMsb);
            return;
        }
        final long ident = Binder.clearCallingIdentity();
        try {
            reportTrace(getOrCreateReporterService(component), params);
        } finally {
            Binder.restoreCallingIdentity(ident);
        }
    }

    private void reportTrace(
            @NonNull ServiceConnector<IMessenger> reporterService,
            @NonNull TraceReportParams params) {
        reporterService.post(messenger -> {
            if (params.usePipeForTesting) {
                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
                try (AutoCloseInputStream i = new AutoCloseInputStream(params.fd)) {
                    try (AutoCloseOutputStream o = new AutoCloseOutputStream(pipe[1])) {
                        byte[] array = i.readNBytes(MAX_FILE_SIZE_BYTES_TO_PIPE);
                        if (array.length == MAX_FILE_SIZE_BYTES_TO_PIPE) {
                            throw new IllegalArgumentException(
                                    "Trace file too large when |usePipeForTesting| is set.");
                        }
                        o.write(array);
                    }
                }
                params.fd = pipe[0];
            }

            Message message = Message.obtain();
            message.what = TraceReportService.MSG_REPORT_TRACE;
            message.obj = params;
            messenger.send(message);

            FrameworkStatsLog.write(TRACING_SERVICE_REPORT_EVENT, REPORT_SVC_HANDOFF,
                    params.uuidLsb, params.uuidMsb);
        }).whenComplete((res, err) -> {
            if (err != null) {
                FrameworkStatsLog.write(TRACING_SERVICE_REPORT_EVENT, REPORT_SVC_COMM_ERROR,
                        params.uuidLsb, params.uuidMsb);
                Slog.e(TAG, "Failed to report trace", err);
            }
            try {
                params.fd.close();
            } catch (IOException ignored) {
            }
        });
    }

    private ServiceConnector<IMessenger> getOrCreateReporterService(
            @NonNull ComponentName component) {
        ServiceConnector<IMessenger> connector = mCachedReporterServices.get(component);
        if (connector == null) {
            Intent intent = new Intent();
            intent.setComponent(component);
            connector = new ServiceConnector.Impl<IMessenger>(
                    mContext, intent,
                    Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY,
                    mContext.getUser().getIdentifier(), IMessenger.Stub::asInterface) {
                private static final long DISCONNECT_TIMEOUT_MS = 15_000;
                private static final long REQUEST_TIMEOUT_MS = 10_000;

                @Override
                protected long getAutoDisconnectTimeoutMs() {
                    return DISCONNECT_TIMEOUT_MS;
                }

                @Override
                protected long getRequestTimeoutMs() {
                    return REQUEST_TIMEOUT_MS;
                }
            };
            mCachedReporterServices.put(intent.getComponent(), connector);
        }
        return connector;
    }

    private boolean hasPermission(@NonNull ComponentName componentName,
            @NonNull String permission) throws SecurityException {
        if (mPackageManager.checkPermission(permission, componentName.getPackageName())
                != PackageManager.PERMISSION_GRANTED) {
            Slog.e(TAG,
                    "Trace reporting service " + componentName.toShortString() + " does not have "
                            + permission + " permission");
            return false;
        }
        return true;
    }

    private boolean hasBindServicePermission(@NonNull ComponentName componentName) {
        ServiceInfo info;
        try {
            info = mPackageManager.getServiceInfo(componentName, 0);
        } catch (NameNotFoundException ex) {
            Slog.e(TAG,
                    "Trace reporting service " + componentName.toShortString() + " does not exist");
            return false;
        }
        if (!Manifest.permission.BIND_TRACE_REPORT_SERVICE.equals(info.permission)) {
            Slog.e(TAG,
                    "Trace reporting service " + componentName.toShortString()
                            + " does not request " + Manifest.permission.BIND_TRACE_REPORT_SERVICE
                            + " permission; instead requests " + info.permission);
            return false;
        }
        return true;
    }
}
