/*
 * Copyright (C) 2018 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.incident;

import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.IIncidentAuthListener;
import android.os.IIncidentCompanion;
import android.os.IIncidentManager;
import android.os.IncidentManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;

import com.android.internal.util.DumpUtils;
import com.android.server.SystemService;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.List;

/**
 * Helper service for incidentd and dumpstated to provide user feedback
 * and authorization for bug and inicdent reports to be taken.
 */
public class IncidentCompanionService extends SystemService {
    static final String TAG = "IncidentCompanionService";

    /**
     * Dump argument for proxying restricted image dumps to the services
     * listed in the config.
     */
    private static String[] RESTRICTED_IMAGE_DUMP_ARGS = new String[] {
        "--hal", "--restricted_image" };

    /**
     * The two permissions, for sendBroadcastAsUserMultiplePermissions.
     */
    private static final String[] DUMP_AND_USAGE_STATS_PERMISSIONS = new String[] {
        android.Manifest.permission.DUMP,
        android.Manifest.permission.PACKAGE_USAGE_STATS
    };

    /**
     * Tracker for reports pending approval.
     */
    private PendingReports mPendingReports;

    /**
     * Implementation of the IIncidentCompanion binder interface.
     */
    private final class BinderService extends IIncidentCompanion.Stub {
        /**
         * ONEWAY binder call to initiate authorizing the report. If you don't need
         * IncidentCompanionService to check whether the calling UID matches then
         * pass 0 for callingUid.  Either way, the caller must have DUMP and USAGE_STATS
         * permissions to retrieve the data, so it ends up being about the same.
         */
        @Override
        public void authorizeReport(int callingUid, final String callingPackage,
                final String receiverClass, final String reportId,
                final int flags, final IIncidentAuthListener listener) {
            enforceRequestAuthorizationPermission();

            final long ident = Binder.clearCallingIdentity();
            try {
                mPendingReports.authorizeReport(callingUid, callingPackage,
                        receiverClass, reportId, flags, listener);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        /**
         * ONEWAY binder call to cancel the inbound authorization request.
         * <p>
         * This is a oneway call, and so is authorizeReport, so the
         * caller's ordering is preserved.  The other calls on this object are synchronous, so
         * their ordering is not guaranteed with respect to these calls.  So the implementation
         * sends out extra broadcasts to allow for eventual consistency.
         */
        public void cancelAuthorization(final IIncidentAuthListener listener) {
            enforceRequestAuthorizationPermission();

            // Caller can cancel if they don't want it anymore, and mRequestQueue elides
            // authorize/cancel pairs.
            final long ident = Binder.clearCallingIdentity();
            try {
                mPendingReports.cancelAuthorization(listener);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        /**
         * ONEWAY implementation to send broadcast from incidentd, which is native.
         */
        @Override
        public void sendReportReadyBroadcast(String pkg, String cls) {
            enforceRequestAuthorizationPermission();

            final long ident = Binder.clearCallingIdentity();
            try {
                final Context context = getContext();

                final int primaryUser = getAndValidateUser(context);
                if (primaryUser == UserHandle.USER_NULL) {
                    return;
                }

                final Intent intent = new Intent(Intent.ACTION_INCIDENT_REPORT_READY);
                intent.setComponent(new ComponentName(pkg, cls));

                Log.d(TAG, "sendReportReadyBroadcast sending primaryUser=" + primaryUser
                        + " userHandle=" + UserHandle.getUserHandleForUid(primaryUser)
                        + " intent=" + intent);

                // Send it to the primary user.  Only they can do incident reports.
                context.sendBroadcastAsUserMultiplePermissions(intent,
                        UserHandle.getUserHandleForUid(primaryUser),
                        DUMP_AND_USAGE_STATS_PERMISSIONS);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        /**
         * SYNCHRONOUS binder call to get the list of reports that are pending confirmation
         * by the user.
         */
        @Override
        public List<String> getPendingReports() {
            enforceAuthorizePermission();
            return mPendingReports.getPendingReports();
        }

        /**
         * SYNCHRONOUS binder call to mark a report as approved.
         */
        @Override
        public void approveReport(String uri) {
            enforceAuthorizePermission();

            final long ident = Binder.clearCallingIdentity();
            try {
                mPendingReports.approveReport(uri);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        /**
         * SYNCHRONOUS binder call to mark a report as NOT approved.
         */
        @Override
        public void denyReport(String uri) {
            enforceAuthorizePermission();

            final long ident = Binder.clearCallingIdentity();
            try {
                mPendingReports.denyReport(uri);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        /**
         * SYNCHRONOUS binder call to get the list of incident reports waiting for a receiver.
         */
        @Override
        public List<String> getIncidentReportList(String pkg, String cls) throws RemoteException {
            enforceAccessReportsPermissions(null);

            final long ident = Binder.clearCallingIdentity();
            try {
                return getIIncidentManager().getIncidentReportList(pkg, cls);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        /**
         * SYNCHRONOUS binder call to commit an incident report
         */
        @Override
        public void deleteIncidentReports(String pkg, String cls, String id)
                throws RemoteException {
            if (pkg == null || cls == null || id == null
                    || pkg.length() == 0 || cls.length() == 0 || id.length() == 0) {
                throw new RuntimeException("Invalid pkg, cls or id");
            }
            enforceAccessReportsPermissions(pkg);

            final long ident = Binder.clearCallingIdentity();
            try {
                getIIncidentManager().deleteIncidentReports(pkg, cls, id);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        /**
         * SYNCHRONOUS binder call to delete all incident reports for a package.
         */
        @Override
        public void deleteAllIncidentReports(String pkg) throws RemoteException {
            if (pkg == null || pkg.length() == 0) {
                throw new RuntimeException("Invalid pkg");
            }
            enforceAccessReportsPermissions(pkg);

            final long ident = Binder.clearCallingIdentity();
            try {
                getIIncidentManager().deleteAllIncidentReports(pkg);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        /**
         * SYNCHRONOUS binder call to get the IncidentReport object.
         */
        @Override
        public IncidentManager.IncidentReport getIncidentReport(String pkg, String cls, String id)
                throws RemoteException {
            if (pkg == null || cls == null || id == null
                    || pkg.length() == 0 || cls.length() == 0 || id.length() == 0) {
                throw new RuntimeException("Invalid pkg, cls or id");
            }
            enforceAccessReportsPermissions(pkg);

            final long ident = Binder.clearCallingIdentity();
            try {
                return getIIncidentManager().getIncidentReport(pkg, cls, id);
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }

        /**
         * SYNCHRONOUS implementation of adb shell dumpsys debugreportcompanion.
         */
        @Override
        protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) {
            if (!DumpUtils.checkDumpPermission(getContext(), TAG, writer)) {
                return;
            }

            if (args.length == 1 && "--restricted_image".equals(args[0])) {
                // Does NOT clearCallingIdentity
                dumpRestrictedImages(fd);
            } else {
                // Regular dump
                mPendingReports.dump(fd, writer, args);
            }
        }

        /**
         * Proxy for the restricted images section.
         */
        private void dumpRestrictedImages(FileDescriptor fd) {
            // Only supported on eng or userdebug.
            if (!(Build.IS_ENG || Build.IS_USERDEBUG)) {
                return;
            }

            final Resources res = getContext().getResources();
            final String[] services = res.getStringArray(
                    com.android.internal.R.array.config_restrictedImagesServices);
            final int servicesCount = services.length;
            for (int i = 0; i < servicesCount; i++) {
                final String name = services[i];
                Log.d(TAG, "Looking up service " + name);
                final IBinder service = ServiceManager.getService(name);
                if (service != null) {
                    Log.d(TAG, "Calling dump on service: " + name);
                    try {
                        service.dump(fd, RESTRICTED_IMAGE_DUMP_ARGS);
                    } catch (RemoteException ex) {
                        Log.w(TAG, "dump --restricted_image of " + name + " threw", ex);
                    }
                }
            }
        }

        /**
         * Inside the binder interface class because we want to do all of the authorization
         * here, before calling out to the helper objects.
         */
        private void enforceRequestAuthorizationPermission() {
            getContext().enforceCallingOrSelfPermission(
                    android.Manifest.permission.REQUEST_INCIDENT_REPORT_APPROVAL, null);
        }

        /**
         * Inside the binder interface class because we want to do all of the authorization
         * here, before calling out to the helper objects.
         */
        private void enforceAuthorizePermission() {
            getContext().enforceCallingOrSelfPermission(
                    android.Manifest.permission.APPROVE_INCIDENT_REPORTS, null);
        }

        /**
         * Enforce that the calling process either has APPROVE_INCIDENT_REPORTS or
         * (DUMP and PACKAGE_USAGE_STATS). This lets the approver get, because showing
         * information about the report is a prerequisite for letting the user decide.
         *
         * If pkg is null, it is not checked, so make sure that you check it for null first
         * if you do need the packages to match.
         *
         * Inside the binder interface class because we want to do all of the authorization
         * here, before calling out to the helper objects.
         */
        private void enforceAccessReportsPermissions(String pkg) {
            if (getContext().checkCallingPermission(
                        android.Manifest.permission.APPROVE_INCIDENT_REPORTS)
                    != PackageManager.PERMISSION_GRANTED) {
                getContext().enforceCallingOrSelfPermission(
                        android.Manifest.permission.DUMP, null);
                getContext().enforceCallingOrSelfPermission(
                        android.Manifest.permission.PACKAGE_USAGE_STATS, null);
                if (pkg != null) {
                    enforceCallerIsSameApp(pkg);
                }
            }
        }

        /**
         * Throw a SecurityException if the incoming binder call is not from pkg.
         */
        private void enforceCallerIsSameApp(String pkg) throws SecurityException {
            try {
                final int uid = Binder.getCallingUid();
                final int userId = UserHandle.getCallingUserId();
                final ApplicationInfo ai = getContext().getPackageManager()
                        .getApplicationInfoAsUser(pkg, 0, userId);
                if (ai == null) {
                    throw new SecurityException("Unknown package " + pkg);
                }
                if (!UserHandle.isSameApp(ai.uid, uid)) {
                    throw new SecurityException("Calling uid " + uid + " gave package "
                            + pkg + " which is owned by uid " + ai.uid);
                }
            } catch (PackageManager.NameNotFoundException re) {
                throw new SecurityException("Unknown package " + pkg + "\n" + re);
            }
        }
    }

    /**
     * Construct new IncidentCompanionService with the context.
     */
    public IncidentCompanionService(Context context) {
        super(context);
        mPendingReports = new PendingReports(context);
    }

    /**
     * Initialize the service.  It is still not safe to do UI until
     * onBootPhase(SystemService.PHASE_BOOT_COMPLETED).
     */
    @Override
    public void onStart() {
        publishBinderService(Context.INCIDENT_COMPANION_SERVICE, new BinderService());
    }

    /**
     * Handle the boot process... Starts everything running once the system is
     * up enough for us to do UI.
     */
    @Override
    public void onBootPhase(int phase) {
        super.onBootPhase(phase);
        switch (phase) {
            case SystemService.PHASE_BOOT_COMPLETED:
                mPendingReports.onBootCompleted();
                break;
        }
    }

    /**
     * Looks up incidentd every time, so we don't need a complex handshake between
     * incidentd and IncidentCompanionService.
     */
    private IIncidentManager getIIncidentManager() throws RemoteException {
        return IIncidentManager.Stub.asInterface(
                ServiceManager.getService(Context.INCIDENT_SERVICE));
    }

    /**
     * Check whether the current user is the primary user, and return the user id if they are.
     * Returns UserHandle.USER_NULL if not valid.
     */
    public static int getAndValidateUser(Context context) {
        // Current user
        UserInfo currentUser;
        try {
            currentUser = ActivityManager.getService().getCurrentUser();
        } catch (RemoteException ex) {
            // We're already inside the system process.
            throw new RuntimeException(ex);
        }

        // Primary user
        final UserManager um = UserManager.get(context);
        final UserInfo primaryUser = um.getPrimaryUser();

        // Check that we're using the right user.
        if (currentUser == null) {
            Log.w(TAG, "No current user.  Nobody to approve the report."
                    + " The report will be denied.");
            return UserHandle.USER_NULL;
        }
        if (primaryUser == null) {
            Log.w(TAG, "No primary user.  Nobody to approve the report."
                    + " The report will be denied.");
            return UserHandle.USER_NULL;
        }
        if (primaryUser.id != currentUser.id) {
            Log.w(TAG, "Only the primary user can approve bugreports, but they are not"
                    + " the current user. The report will be denied.");
            return UserHandle.USER_NULL;
        }

        return primaryUser.id;
    }
}

