/*
 * 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.contentcapture;

import static android.Manifest.permission.MANAGE_CONTENT_CAPTURE;
import static android.content.Context.CONTENT_CAPTURE_MANAGER_SERVICE;
import static android.service.contentcapture.ContentCaptureService.setClientState;
import static android.view.contentcapture.ContentCaptureHelper.toList;
import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALSE;
import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_OK;
import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_SECURITY_EXCEPTION;
import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_TRUE;
import static android.view.contentcapture.ContentCaptureSession.STATE_DISABLED;

import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ACCEPT_DATA_SHARE_REQUEST;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_CLIENT_PIPE_FAIL;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_CONCURRENT_REQUEST;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_EMPTY_DATA;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_IOEXCEPTION;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_SERVICE_PIPE_FAIL;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_TIMEOUT_INTERRUPTED;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_WRITE_FINISHED;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__REJECT_DATA_SHARE_REQUEST;
import static com.android.internal.util.SyncResultReceiver.bundleFor;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManagerInternal;
import android.app.ActivityThread;
import android.content.ComponentName;
import android.content.ContentCaptureOptions;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ActivityPresentationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.provider.Settings;
import android.service.contentcapture.ActivityEvent.ActivityEventType;
import android.service.contentcapture.IDataShareCallback;
import android.service.contentcapture.IDataShareReadAdapter;
import android.util.ArraySet;
import android.util.LocalLog;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.view.contentcapture.ContentCaptureCondition;
import android.view.contentcapture.ContentCaptureHelper;
import android.view.contentcapture.ContentCaptureManager;
import android.view.contentcapture.DataRemovalRequest;
import android.view.contentcapture.DataShareRequest;
import android.view.contentcapture.IContentCaptureManager;
import android.view.contentcapture.IDataShareWriteAdapter;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.infra.AbstractRemoteService;
import com.android.internal.infra.GlobalWhitelistState;
import com.android.internal.os.IResultReceiver;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.Preconditions;
import com.android.server.LocalServices;
import com.android.server.infra.AbstractMasterSystemService;
import com.android.server.infra.FrameworkResourcesServiceNameResolver;

import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * A service used to observe the contents of the screen.
 *
 * <p>The data collected by this service can be analyzed on-device and combined
 * with other sources to provide contextual data in other areas of the system
 * such as Autofill.
 */
public final class ContentCaptureManagerService extends
        AbstractMasterSystemService<ContentCaptureManagerService, ContentCapturePerUserService> {

    private static final String TAG = ContentCaptureManagerService.class.getSimpleName();
    static final String RECEIVER_BUNDLE_EXTRA_SESSIONS = "sessions";

    private static final int MAX_TEMP_SERVICE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes
    private static final int MAX_DATA_SHARE_FILE_DESCRIPTORS_TTL_MS =  1_000 * 60 * 5; // 5 minutes
    private static final int MAX_CONCURRENT_FILE_SHARING_REQUESTS = 10;
    private static final int DATA_SHARE_BYTE_BUFFER_LENGTH = 1_024;

    // Needed to pass checkstyle_hook as names are too long for one line.
    private static final int EVENT__DATA_SHARE_ERROR_CONCURRENT_REQUEST =
            CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_CONCURRENT_REQUEST;
    private static final int EVENT__DATA_SHARE_ERROR_TIMEOUT_INTERRUPTED =
            CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_TIMEOUT_INTERRUPTED;
    private static final int EVENT__DATA_SHARE_WRITE_FINISHED =
            CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_WRITE_FINISHED;

    private final LocalService mLocalService = new LocalService();

    @Nullable
    final LocalLog mRequestsHistory;

    @GuardedBy("mLock")
    private ActivityManagerInternal mAm;

    /**
     * Users disabled by {@link android.provider.Settings.Secure#CONTENT_CAPTURE_ENABLED}
     */
    @GuardedBy("mLock")
    @Nullable
    private SparseBooleanArray mDisabledBySettings;

    /**
     * Global kill-switch based on value defined by
     * {@link ContentCaptureManager#DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED}.
     */
    @GuardedBy("mLock")
    @Nullable
    private boolean mDisabledByDeviceConfig;

    // Device-config settings that are cached and passed back to apps
    @GuardedBy("mLock") int mDevCfgLoggingLevel;
    @GuardedBy("mLock") int mDevCfgMaxBufferSize;
    @GuardedBy("mLock") int mDevCfgIdleFlushingFrequencyMs;
    @GuardedBy("mLock") int mDevCfgTextChangeFlushingFrequencyMs;
    @GuardedBy("mLock") int mDevCfgLogHistorySize;
    @GuardedBy("mLock") int mDevCfgIdleUnbindTimeoutMs;

    private final Executor mDataShareExecutor = Executors.newCachedThreadPool();
    private final Handler mHandler = new Handler(Looper.getMainLooper());

    @GuardedBy("mLock")
    private final Set<String> mPackagesWithShareRequests = new HashSet<>();

    final GlobalContentCaptureOptions mGlobalContentCaptureOptions =
            new GlobalContentCaptureOptions();

    public ContentCaptureManagerService(@NonNull Context context) {
        super(context, new FrameworkResourcesServiceNameResolver(context,
                com.android.internal.R.string.config_defaultContentCaptureService),
                UserManager.DISALLOW_CONTENT_CAPTURE,
                /*packageUpdatePolicy=*/ PACKAGE_UPDATE_POLICY_NO_REFRESH);
        DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                ActivityThread.currentApplication().getMainExecutor(),
                (properties) -> onDeviceConfigChange(properties));
        setDeviceConfigProperties();

        if (mDevCfgLogHistorySize > 0) {
            if (debug) Slog.d(TAG, "log history size: " + mDevCfgLogHistorySize);
            mRequestsHistory = new LocalLog(mDevCfgLogHistorySize);
        } else {
            if (debug) {
                Slog.d(TAG, "disabled log history because size is " + mDevCfgLogHistorySize);
            }
            mRequestsHistory = null;
        }

        final List<UserInfo> users = getSupportedUsers();
        for (int i = 0; i < users.size(); i++) {
            final int userId = users.get(i).id;
            final boolean disabled = !isEnabledBySettings(userId);
            // Sets which services are disabled by settings
            if (disabled) {
                Slog.i(TAG, "user " + userId + " disabled by settings");
                if (mDisabledBySettings == null) {
                    mDisabledBySettings = new SparseBooleanArray(1);
                }
                mDisabledBySettings.put(userId, true);
            }
            // Sets the global options for the service.
            mGlobalContentCaptureOptions.setServiceInfo(userId,
                    mServiceNameResolver.getServiceName(userId),
                    mServiceNameResolver.isTemporary(userId));
        }
    }

    @Override // from AbstractMasterSystemService
    protected ContentCapturePerUserService newServiceLocked(@UserIdInt int resolvedUserId,
            boolean disabled) {
        return new ContentCapturePerUserService(this, mLock, disabled, resolvedUserId);
    }

    @Override // from SystemService
    public boolean isUserSupported(TargetUser user) {
        return user.getUserInfo().isFull() || user.getUserInfo().isManagedProfile();
    }

    @Override // from SystemService
    public void onStart() {
        publishBinderService(CONTENT_CAPTURE_MANAGER_SERVICE,
                new ContentCaptureManagerServiceStub());
        publishLocalService(ContentCaptureManagerInternal.class, mLocalService);
    }

    @Override // from AbstractMasterSystemService
    protected void onServiceRemoved(@NonNull ContentCapturePerUserService service,
            @UserIdInt int userId) {
        service.destroyLocked();
    }

    @Override // from AbstractMasterSystemService
    protected void onServicePackageUpdatingLocked(int userId) {
        final ContentCapturePerUserService service = getServiceForUserLocked(userId);
        if (service != null) {
            service.onPackageUpdatingLocked();
        }
    }

    @Override // from AbstractMasterSystemService
    protected void onServicePackageUpdatedLocked(@UserIdInt int userId) {
        final ContentCapturePerUserService service = getServiceForUserLocked(userId);
        if (service != null) {
            service.onPackageUpdatedLocked();
        }
    }

    @Override // from AbstractMasterSystemService
    protected void onServiceNameChanged(@UserIdInt int userId, @NonNull String serviceName,
            boolean isTemporary) {
        mGlobalContentCaptureOptions.setServiceInfo(userId, serviceName, isTemporary);

        super.onServiceNameChanged(userId, serviceName, isTemporary);
    }

    @Override // from AbstractMasterSystemService
    protected void enforceCallingPermissionForManagement() {
        getContext().enforceCallingPermission(MANAGE_CONTENT_CAPTURE, TAG);
    }

    @Override // from AbstractMasterSystemService
    protected int getMaximumTemporaryServiceDurationMs() {
        return MAX_TEMP_SERVICE_DURATION_MS;
    }

    @Override // from AbstractMasterSystemService
    protected void registerForExtraSettingsChanges(@NonNull ContentResolver resolver,
            @NonNull ContentObserver observer) {
        resolver.registerContentObserver(Settings.Secure.getUriFor(
                Settings.Secure.CONTENT_CAPTURE_ENABLED), false, observer,
                UserHandle.USER_ALL);
    }

    @Override // from AbstractMasterSystemService
    protected void onSettingsChanged(@UserIdInt int userId, @NonNull String property) {
        switch (property) {
            case Settings.Secure.CONTENT_CAPTURE_ENABLED:
                setContentCaptureFeatureEnabledBySettingsForUser(userId,
                        isEnabledBySettings(userId));
                return;
            default:
                Slog.w(TAG, "Unexpected property (" + property + "); updating cache instead");
        }
    }

    @Override // from AbstractMasterSystemService
    protected boolean isDisabledLocked(@UserIdInt int userId) {
        return mDisabledByDeviceConfig || isDisabledBySettingsLocked(userId)
                || super.isDisabledLocked(userId);
    }

    private boolean isDisabledBySettingsLocked(@UserIdInt int userId) {
        return mDisabledBySettings != null && mDisabledBySettings.get(userId);
    }

    private boolean isEnabledBySettings(@UserIdInt int userId) {
        final boolean enabled = Settings.Secure.getIntForUser(getContext().getContentResolver(),
                Settings.Secure.CONTENT_CAPTURE_ENABLED, 1, userId) == 1 ? true : false;
        return enabled;
    }

    private void onDeviceConfigChange(@NonNull Properties properties) {
        for (String key : properties.getKeyset()) {
            switch (key) {
                case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED:
                    setDisabledByDeviceConfig(properties.getString(key, null));
                    return;
                case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL:
                    setLoggingLevelFromDeviceConfig();
                    return;
                case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE:
                case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY:
                case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE:
                case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY:
                case ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT:
                    setFineTuneParamsFromDeviceConfig();
                    return;
                default:
                    Slog.i(TAG, "Ignoring change on " + key);
            }
        }
    }

    private void setFineTuneParamsFromDeviceConfig() {
        synchronized (mLock) {
            mDevCfgMaxBufferSize = DeviceConfig.getInt(
                    DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                    ContentCaptureManager.DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE,
                    ContentCaptureManager.DEFAULT_MAX_BUFFER_SIZE);
            mDevCfgIdleFlushingFrequencyMs = DeviceConfig.getInt(
                    DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                    ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY,
                    ContentCaptureManager.DEFAULT_IDLE_FLUSHING_FREQUENCY_MS);
            mDevCfgTextChangeFlushingFrequencyMs = DeviceConfig.getInt(
                    DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                    ContentCaptureManager.DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY,
                    ContentCaptureManager.DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS);
            mDevCfgLogHistorySize = DeviceConfig.getInt(
                    DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                    ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE, 20);
            mDevCfgIdleUnbindTimeoutMs = DeviceConfig.getInt(
                    DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                    ContentCaptureManager.DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT,
                    (int) AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS);
            if (verbose) {
                Slog.v(TAG, "setFineTuneParamsFromDeviceConfig(): "
                        + "bufferSize=" + mDevCfgMaxBufferSize
                        + ", idleFlush=" + mDevCfgIdleFlushingFrequencyMs
                        + ", textFluxh=" + mDevCfgTextChangeFlushingFrequencyMs
                        + ", logHistory=" + mDevCfgLogHistorySize
                        + ", idleUnbindTimeoutMs=" + mDevCfgIdleUnbindTimeoutMs);
            }
        }
    }

    private void setLoggingLevelFromDeviceConfig() {
        mDevCfgLoggingLevel = DeviceConfig.getInt(
                DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                ContentCaptureManager.DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL,
                ContentCaptureHelper.getDefaultLoggingLevel());
        ContentCaptureHelper.setLoggingLevel(mDevCfgLoggingLevel);
        verbose = ContentCaptureHelper.sVerbose;
        debug = ContentCaptureHelper.sDebug;
        if (verbose) {
            Slog.v(TAG, "setLoggingLevelFromDeviceConfig(): level=" + mDevCfgLoggingLevel
                    + ", debug=" + debug + ", verbose=" + verbose);
        }
    }

    private void setDeviceConfigProperties() {
        setLoggingLevelFromDeviceConfig();
        setFineTuneParamsFromDeviceConfig();
        final String enabled = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
                ContentCaptureManager.DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED);
        setDisabledByDeviceConfig(enabled);
    }

    private void setDisabledByDeviceConfig(@Nullable String explicitlyEnabled) {
        if (verbose) {
            Slog.v(TAG, "setDisabledByDeviceConfig(): explicitlyEnabled=" + explicitlyEnabled);
        }
        final List<UserInfo> users = getSupportedUsers();

        final boolean newDisabledValue;

        if (explicitlyEnabled != null && explicitlyEnabled.equalsIgnoreCase("false")) {
            newDisabledValue = true;
        } else {
            newDisabledValue = false;
        }

        synchronized (mLock) {
            if (mDisabledByDeviceConfig == newDisabledValue) {
                if (verbose) {
                    Slog.v(TAG, "setDisabledByDeviceConfig(): already " + newDisabledValue);
                }
                return;
            }
            mDisabledByDeviceConfig = newDisabledValue;

            Slog.i(TAG, "setDisabledByDeviceConfig(): set to " + mDisabledByDeviceConfig);
            for (int i = 0; i < users.size(); i++) {
                final int userId = users.get(i).id;
                boolean disabled = mDisabledByDeviceConfig || isDisabledBySettingsLocked(userId);
                Slog.i(TAG, "setDisabledByDeviceConfig(): updating service for user "
                        + userId + " to " + (disabled ? "'disabled'" : "'enabled'"));
                updateCachedServiceLocked(userId, disabled);
            }
        }
    }

    private void setContentCaptureFeatureEnabledBySettingsForUser(@UserIdInt int userId,
            boolean enabled) {
        synchronized (mLock) {
            if (mDisabledBySettings == null) {
                mDisabledBySettings = new SparseBooleanArray();
            }
            final boolean alreadyEnabled = !mDisabledBySettings.get(userId);
            if (!(enabled ^ alreadyEnabled)) {
                if (debug) {
                    Slog.d(TAG, "setContentCaptureFeatureEnabledForUser(): already " + enabled);
                }
                return;
            }
            if (enabled) {
                Slog.i(TAG, "setContentCaptureFeatureEnabled(): enabling service for user "
                        + userId);
                mDisabledBySettings.delete(userId);
            } else {
                Slog.i(TAG, "setContentCaptureFeatureEnabled(): disabling service for user "
                        + userId);
                mDisabledBySettings.put(userId, true);
            }
            final boolean disabled = !enabled || mDisabledByDeviceConfig;
            updateCachedServiceLocked(userId, disabled);
        }
    }

    // Called by Shell command.
    void destroySessions(@UserIdInt int userId, @NonNull IResultReceiver receiver) {
        Slog.i(TAG, "destroySessions() for userId " + userId);
        enforceCallingPermissionForManagement();

        synchronized (mLock) {
            if (userId != UserHandle.USER_ALL) {
                final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
                if (service != null) {
                    service.destroySessionsLocked();
                }
            } else {
                visitServicesLocked((s) -> s.destroySessionsLocked());
            }
        }

        try {
            receiver.send(0, new Bundle());
        } catch (RemoteException e) {
            // Just ignore it...
        }
    }

    // Called by Shell command.
    void listSessions(int userId, IResultReceiver receiver) {
        Slog.i(TAG, "listSessions() for userId " + userId);
        enforceCallingPermissionForManagement();

        final Bundle resultData = new Bundle();
        final ArrayList<String> sessions = new ArrayList<>();

        synchronized (mLock) {
            if (userId != UserHandle.USER_ALL) {
                final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
                if (service != null) {
                    service.listSessionsLocked(sessions);
                }
            } else {
                visitServicesLocked((s) -> s.listSessionsLocked(sessions));
            }
        }

        resultData.putStringArrayList(RECEIVER_BUNDLE_EXTRA_SESSIONS, sessions);
        try {
            receiver.send(0, resultData);
        } catch (RemoteException e) {
            // Just ignore it...
        }
    }

    private ActivityManagerInternal getAmInternal() {
        synchronized (mLock) {
            if (mAm == null) {
                mAm = LocalServices.getService(ActivityManagerInternal.class);
            }
        }
        return mAm;
    }

    @GuardedBy("mLock")
    private void assertCalledByServiceLocked(@NonNull String methodName) {
        if (!isCalledByServiceLocked(methodName)) {
            throw new SecurityException("caller is not user's ContentCapture service");
        }
    }

    @GuardedBy("mLock")
    private boolean isCalledByServiceLocked(@NonNull String methodName) {
        final int userId = UserHandle.getCallingUserId();
        final int callingUid = Binder.getCallingUid();
        final String serviceName = mServiceNameResolver.getServiceName(userId);
        if (serviceName == null) {
            Slog.e(TAG, methodName + ": called by UID " + callingUid
                    + ", but there's no service set for user " + userId);
            return false;
        }

        final ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName);
        if (serviceComponent == null) {
            Slog.w(TAG, methodName + ": invalid service name: " + serviceName);
            return false;
        }

        final String servicePackageName = serviceComponent.getPackageName();

        final PackageManager pm = getContext().getPackageManager();
        final int serviceUid;
        try {
            serviceUid = pm.getPackageUidAsUser(servicePackageName, UserHandle.getCallingUserId());
        } catch (NameNotFoundException e) {
            Slog.w(TAG, methodName + ": could not verify UID for " + serviceName);
            return false;
        }
        if (callingUid != serviceUid) {
            Slog.e(TAG, methodName + ": called by UID " + callingUid + ", but service UID is "
                    + serviceUid);
            return false;
        }

        return true;
    }

    /**
     * Executes the given {@code runnable} and if it throws a {@link SecurityException},
     * send it back to the receiver.
     *
     * @return whether the exception was thrown or not.
     */
    private boolean throwsSecurityException(@NonNull IResultReceiver result,
            @NonNull Runnable runable) {
        try {
            runable.run();
            return false;
        } catch (SecurityException e) {
            try {
                result.send(RESULT_CODE_SECURITY_EXCEPTION, bundleFor(e.getMessage()));
            } catch (RemoteException e2) {
                Slog.w(TAG, "Unable to send security exception (" + e + "): ", e2);
            }
        }
        return true;
    }

    @GuardedBy("mLock")
    private boolean isDefaultServiceLocked(int userId) {
        final String defaultServiceName = mServiceNameResolver.getDefaultServiceName(userId);
        if (defaultServiceName == null) {
            return false;
        }

        final String currentServiceName = mServiceNameResolver.getServiceName(userId);
        return defaultServiceName.equals(currentServiceName);
    }

    @Override // from AbstractMasterSystemService
    protected void dumpLocked(String prefix, PrintWriter pw) {
        super.dumpLocked(prefix, pw);

        final String prefix2 = prefix + "  ";

        pw.print(prefix); pw.print("Users disabled by Settings: "); pw.println(mDisabledBySettings);
        pw.print(prefix); pw.println("DeviceConfig Settings: ");
        pw.print(prefix2); pw.print("disabled: "); pw.println(mDisabledByDeviceConfig);
        pw.print(prefix2); pw.print("loggingLevel: "); pw.println(mDevCfgLoggingLevel);
        pw.print(prefix2); pw.print("maxBufferSize: "); pw.println(mDevCfgMaxBufferSize);
        pw.print(prefix2); pw.print("idleFlushingFrequencyMs: ");
        pw.println(mDevCfgIdleFlushingFrequencyMs);
        pw.print(prefix2); pw.print("textChangeFlushingFrequencyMs: ");
        pw.println(mDevCfgTextChangeFlushingFrequencyMs);
        pw.print(prefix2); pw.print("logHistorySize: "); pw.println(mDevCfgLogHistorySize);
        pw.print(prefix2); pw.print("idleUnbindTimeoutMs: ");
        pw.println(mDevCfgIdleUnbindTimeoutMs);
        pw.print(prefix); pw.println("Global Options:");
        mGlobalContentCaptureOptions.dump(prefix2, pw);
    }

    final class ContentCaptureManagerServiceStub extends IContentCaptureManager.Stub {

        @Override
        public void startSession(@NonNull IBinder activityToken,
                @NonNull ComponentName componentName, int sessionId, int flags,
                @NonNull IResultReceiver result) {
            Preconditions.checkNotNull(activityToken);
            Preconditions.checkNotNull(sessionId);
            final int userId = UserHandle.getCallingUserId();

            final ActivityPresentationInfo activityPresentationInfo = getAmInternal()
                    .getActivityPresentationInfo(activityToken);

            synchronized (mLock) {
                final ContentCapturePerUserService service = getServiceForUserLocked(userId);
                if (!isDefaultServiceLocked(userId) && !isCalledByServiceLocked("startSession()")) {
                    setClientState(result, STATE_DISABLED, /* binder= */ null);
                    return;
                }
                service.startSessionLocked(activityToken, activityPresentationInfo, sessionId,
                        Binder.getCallingUid(), flags, result);
            }
        }

        @Override
        public void finishSession(int sessionId) {
            Preconditions.checkNotNull(sessionId);
            final int userId = UserHandle.getCallingUserId();

            synchronized (mLock) {
                final ContentCapturePerUserService service = getServiceForUserLocked(userId);
                service.finishSessionLocked(sessionId);
            }
        }

        @Override
        public void getServiceComponentName(@NonNull IResultReceiver result) {
            final int userId = UserHandle.getCallingUserId();
            ComponentName connectedServiceComponentName;
            synchronized (mLock) {
                final ContentCapturePerUserService service = getServiceForUserLocked(userId);
                connectedServiceComponentName = service.getServiceComponentName();
            }
            try {
                result.send(RESULT_CODE_OK, bundleFor(connectedServiceComponentName));
            } catch (RemoteException e) {
                Slog.w(TAG, "Unable to send service component name: " + e);
            }
        }

        @Override
        public void removeData(@NonNull DataRemovalRequest request) {
            Preconditions.checkNotNull(request);
            assertCalledByPackageOwner(request.getPackageName());

            final int userId = UserHandle.getCallingUserId();
            synchronized (mLock) {
                final ContentCapturePerUserService service = getServiceForUserLocked(userId);
                service.removeDataLocked(request);
            }
        }

        @Override
        public void shareData(@NonNull DataShareRequest request,
                @NonNull IDataShareWriteAdapter clientAdapter) {
            Preconditions.checkNotNull(request);
            Preconditions.checkNotNull(clientAdapter);

            assertCalledByPackageOwner(request.getPackageName());

            final int userId = UserHandle.getCallingUserId();
            synchronized (mLock) {
                final ContentCapturePerUserService service = getServiceForUserLocked(userId);

                if (mPackagesWithShareRequests.size() >= MAX_CONCURRENT_FILE_SHARING_REQUESTS
                        || mPackagesWithShareRequests.contains(request.getPackageName())) {
                    try {
                        String serviceName = mServiceNameResolver.getServiceName(userId);
                        ContentCaptureMetricsLogger.writeServiceEvent(
                                EVENT__DATA_SHARE_ERROR_CONCURRENT_REQUEST,
                                serviceName, request.getPackageName());
                        clientAdapter.error(
                                ContentCaptureManager.DATA_SHARE_ERROR_CONCURRENT_REQUEST);
                    } catch (RemoteException e) {
                        Slog.e(TAG, "Failed to send error message to client");
                    }
                    return;
                }

                service.onDataSharedLocked(request,
                        new DataShareCallbackDelegate(request, clientAdapter,
                                ContentCaptureManagerService.this));
            }
        }

        @Override
        public void isContentCaptureFeatureEnabled(@NonNull IResultReceiver result) {
            boolean enabled;
            synchronized (mLock) {
                if (throwsSecurityException(result,
                        () -> assertCalledByServiceLocked("isContentCaptureFeatureEnabled()"))) {
                    return;
                }

                final int userId = UserHandle.getCallingUserId();
                enabled = !mDisabledByDeviceConfig && !isDisabledBySettingsLocked(userId);
            }
            try {
                result.send(enabled ? RESULT_CODE_TRUE : RESULT_CODE_FALSE, /* resultData= */null);
            } catch (RemoteException e) {
                Slog.w(TAG, "Unable to send isContentCaptureFeatureEnabled(): " + e);
            }
        }

        @Override
        public void getServiceSettingsActivity(@NonNull IResultReceiver result) {
            if (throwsSecurityException(result, () -> enforceCallingPermissionForManagement())) {
                return;
            }

            final int userId = UserHandle.getCallingUserId();
            final ComponentName componentName;
            synchronized (mLock) {
                final ContentCapturePerUserService service = getServiceForUserLocked(userId);
                if (service == null) return;
                componentName = service.getServiceSettingsActivityLocked();
            }
            try {
                result.send(RESULT_CODE_OK, bundleFor(componentName));
            } catch (RemoteException e) {
                Slog.w(TAG, "Unable to send getServiceSettingsIntent(): " + e);
            }
        }

        @Override
        public void getContentCaptureConditions(@NonNull String packageName,
                @NonNull IResultReceiver result) {
            if (throwsSecurityException(result, () -> assertCalledByPackageOwner(packageName))) {
                return;
            }

            final int userId = UserHandle.getCallingUserId();
            final ArrayList<ContentCaptureCondition> conditions;
            synchronized (mLock) {
                final ContentCapturePerUserService service = getServiceForUserLocked(userId);
                conditions = service == null ? null
                        : toList(service.getContentCaptureConditionsLocked(packageName));
            }
            try {
                result.send(RESULT_CODE_OK, bundleFor(conditions));
            } catch (RemoteException e) {
                Slog.w(TAG, "Unable to send getServiceComponentName(): " + e);
            }
        }

        @Override
        public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
            if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return;

            boolean showHistory = true;
            if (args != null) {
                for (String arg : args) {
                    switch (arg) {
                        case "--no-history":
                            showHistory = false;
                            break;
                        case "--help":
                            pw.println("Usage: dumpsys content_capture [--no-history]");
                            return;
                        default:
                            Slog.w(TAG, "Ignoring invalid dump arg: " + arg);
                    }
                }
            }

            synchronized (mLock) {
                dumpLocked("", pw);
            }
            pw.print("Requests history: ");
            if (mRequestsHistory == null) {
                pw.println("disabled by device config");
            } else if (showHistory) {
                pw.println();
                mRequestsHistory.reverseDump(fd, pw, args);
                pw.println();
            } else {
                pw.println();
            }
        }

        @Override
        public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
                String[] args, ShellCallback callback, ResultReceiver resultReceiver)
                throws RemoteException {
            new ContentCaptureManagerServiceShellCommand(ContentCaptureManagerService.this).exec(
                    this, in, out, err, args, callback, resultReceiver);
        }
    }

    private final class LocalService extends ContentCaptureManagerInternal {

        @Override
        public boolean isContentCaptureServiceForUser(int uid, @UserIdInt int userId) {
            synchronized (mLock) {
                final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
                if (service != null) {
                    return service.isContentCaptureServiceForUserLocked(uid);
                }
            }
            return false;
        }

        @Override
        public boolean sendActivityAssistData(@UserIdInt int userId, @NonNull IBinder activityToken,
                @NonNull Bundle data) {
            synchronized (mLock) {
                final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
                if (service != null) {
                    return service.sendActivityAssistDataLocked(activityToken, data);
                }
            }
            return false;
        }

        @Override
        public ContentCaptureOptions getOptionsForPackage(int userId, @NonNull String packageName) {
            return mGlobalContentCaptureOptions.getOptions(userId, packageName);
        }

        @Override
        public void notifyActivityEvent(int userId, @NonNull ComponentName activityComponent,
                @ActivityEventType int eventType) {
            synchronized (mLock) {
                final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
                if (service != null) {
                    service.onActivityEventLocked(activityComponent, eventType);
                }
            }
        }
    }

    /**
     * Content capture options associated with all services.
     *
     * <p>This object is defined here instead of on each {@link ContentCapturePerUserService}
     * because it cannot hold a lock on the main lock when
     * {@link GlobalContentCaptureOptions#getOptions(int, String)} is called by external services.
     */
    final class GlobalContentCaptureOptions extends GlobalWhitelistState {

        @GuardedBy("mGlobalWhitelistStateLock")
        private final SparseArray<String> mServicePackages = new SparseArray<>();
        @GuardedBy("mGlobalWhitelistStateLock")
        private final SparseBooleanArray mTemporaryServices = new SparseBooleanArray();

        private void setServiceInfo(@UserIdInt int userId, @Nullable String serviceName,
                boolean isTemporary) {
            synchronized (mGlobalWhitelistStateLock) {
                if (isTemporary) {
                    mTemporaryServices.put(userId, true);
                } else {
                    mTemporaryServices.delete(userId);
                }
                if (serviceName != null) {
                    final ComponentName componentName =
                            ComponentName.unflattenFromString(serviceName);
                    if (componentName == null) {
                        Slog.w(TAG, "setServiceInfo(): invalid name: " + serviceName);
                        mServicePackages.remove(userId);
                    } else {
                        mServicePackages.put(userId, componentName.getPackageName());
                    }
                } else {
                    mServicePackages.remove(userId);
                }
            }
        }

        @Nullable
        @GuardedBy("mGlobalWhitelistStateLock")
        public ContentCaptureOptions getOptions(@UserIdInt int userId,
                @NonNull String packageName) {
            boolean packageWhitelisted;
            ArraySet<ComponentName> whitelistedComponents = null;
            synchronized (mGlobalWhitelistStateLock) {
                packageWhitelisted = isWhitelisted(userId, packageName);
                if (!packageWhitelisted) {
                    // Full package is not whitelisted: check individual components first
                    whitelistedComponents = getWhitelistedComponents(userId, packageName);
                    if (whitelistedComponents == null
                            && packageName.equals(mServicePackages.get(userId))) {
                        // No components whitelisted either, but let it go because it's the
                        // service's own package
                        if (verbose) Slog.v(TAG, "getOptionsForPackage() lite for " + packageName);
                        return new ContentCaptureOptions(mDevCfgLoggingLevel);
                    }
                }
            } // synchronized

            // Restrict what temporary services can whitelist
            if (Build.IS_USER && mServiceNameResolver.isTemporary(userId)) {
                if (!packageName.equals(mServicePackages.get(userId))) {
                    Slog.w(TAG, "Ignoring package " + packageName + " while using temporary "
                            + "service " + mServicePackages.get(userId));
                    return null;
                }
            }

            if (!packageWhitelisted && whitelistedComponents == null) {
                // No can do!
                if (verbose) {
                    Slog.v(TAG, "getOptionsForPackage(" + packageName + "): not whitelisted");
                }
                return null;
            }

            final ContentCaptureOptions options = new ContentCaptureOptions(mDevCfgLoggingLevel,
                    mDevCfgMaxBufferSize, mDevCfgIdleFlushingFrequencyMs,
                    mDevCfgTextChangeFlushingFrequencyMs, mDevCfgLogHistorySize,
                    whitelistedComponents);
            if (verbose) Slog.v(TAG, "getOptionsForPackage(" + packageName + "): " + options);
            return options;
        }

        @Override
        public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
            super.dump(prefix, pw);

            synchronized (mGlobalWhitelistStateLock) {
                if (mServicePackages.size() > 0) {
                    pw.print(prefix); pw.print("Service packages: "); pw.println(mServicePackages);
                }
                if (mTemporaryServices.size() > 0) {
                    pw.print(prefix); pw.print("Temp services: "); pw.println(mTemporaryServices);
                }
            }
        }
    }

    private static class DataShareCallbackDelegate extends IDataShareCallback.Stub {

        @NonNull private final DataShareRequest mDataShareRequest;
        @NonNull private final IDataShareWriteAdapter mClientAdapter;
        @NonNull private final ContentCaptureManagerService mParentService;
        @NonNull private final AtomicBoolean mLoggedWriteFinish = new AtomicBoolean(false);

        DataShareCallbackDelegate(@NonNull DataShareRequest dataShareRequest,
                @NonNull IDataShareWriteAdapter clientAdapter,
                ContentCaptureManagerService parentService) {
            mDataShareRequest = dataShareRequest;
            mClientAdapter = clientAdapter;
            mParentService = parentService;
        }

        @Override
        public void accept(@NonNull IDataShareReadAdapter serviceAdapter) {
            Slog.i(TAG, "Data share request accepted by Content Capture service");
            logServiceEvent(CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ACCEPT_DATA_SHARE_REQUEST);

            Pair<ParcelFileDescriptor, ParcelFileDescriptor> clientPipe = createPipe();
            if (clientPipe == null) {
                logServiceEvent(
                        CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_CLIENT_PIPE_FAIL);
                sendErrorSignal(mClientAdapter, serviceAdapter,
                        ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
                return;
            }

            ParcelFileDescriptor sourceIn = clientPipe.second;
            ParcelFileDescriptor sinkIn = clientPipe.first;

            Pair<ParcelFileDescriptor, ParcelFileDescriptor> servicePipe = createPipe();
            if (servicePipe == null) {
                logServiceEvent(
                        CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_SERVICE_PIPE_FAIL);
                bestEffortCloseFileDescriptors(sourceIn, sinkIn);

                sendErrorSignal(mClientAdapter, serviceAdapter,
                        ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
                return;
            }

            ParcelFileDescriptor sourceOut = servicePipe.second;
            ParcelFileDescriptor sinkOut = servicePipe.first;

            mParentService.mPackagesWithShareRequests.add(mDataShareRequest.getPackageName());

            try {
                mClientAdapter.write(sourceIn);
            } catch (RemoteException e) {
                Slog.e(TAG, "Failed to call write() the client operation", e);
                sendErrorSignal(mClientAdapter, serviceAdapter,
                        ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
                logServiceEvent(
                        CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_CLIENT_PIPE_FAIL);
                return;
            }
            try {
                serviceAdapter.start(sinkOut);
            } catch (RemoteException e) {
                Slog.e(TAG, "Failed to call start() the service operation", e);
                sendErrorSignal(mClientAdapter, serviceAdapter,
                        ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
                logServiceEvent(
                        CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_SERVICE_PIPE_FAIL);
                return;
            }

            // File descriptors received by remote apps will be copies of the current one. Close
            // the ones that belong to the system server, so there's only 1 open left for the
            // current pipe. Therefore when remote parties decide to close them - all descriptors
            // pointing to the pipe will be closed.
            bestEffortCloseFileDescriptors(sourceIn, sinkOut);

            mParentService.mDataShareExecutor.execute(() -> {
                boolean receivedData = false;
                try (InputStream fis =
                             new ParcelFileDescriptor.AutoCloseInputStream(sinkIn);
                     OutputStream fos =
                             new ParcelFileDescriptor.AutoCloseOutputStream(sourceOut)) {

                    byte[] byteBuffer = new byte[DATA_SHARE_BYTE_BUFFER_LENGTH];
                    while (true) {
                        int readBytes = fis.read(byteBuffer);

                        if (readBytes == -1) {
                            break;
                        }

                        fos.write(byteBuffer, 0 /* offset */, readBytes);

                        receivedData = true;
                    }
                } catch (IOException e) {
                    Slog.e(TAG, "Failed to pipe client and service streams", e);
                    logServiceEvent(
                            CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_IOEXCEPTION);

                    sendErrorSignal(mClientAdapter, serviceAdapter,
                            ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
                } finally {
                    synchronized (mParentService.mLock) {
                        mParentService.mPackagesWithShareRequests
                                .remove(mDataShareRequest.getPackageName());
                    }
                    if (receivedData) {
                        if (!mLoggedWriteFinish.get()) {
                            logServiceEvent(EVENT__DATA_SHARE_WRITE_FINISHED);
                            mLoggedWriteFinish.set(true);
                        }
                        try {
                            mClientAdapter.finish();
                        } catch (RemoteException e) {
                            Slog.e(TAG, "Failed to call finish() the client operation", e);
                        }
                        try {
                            serviceAdapter.finish();
                        } catch (RemoteException e) {
                            Slog.e(TAG, "Failed to call finish() the service operation", e);
                        }
                    } else {
                        // Client or service may have crashed before sending.
                        logServiceEvent(
                                CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_ERROR_EMPTY_DATA);
                        sendErrorSignal(mClientAdapter, serviceAdapter,
                                ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
                    }
                }
            });

            mParentService.mHandler.postDelayed(() ->
                    enforceDataSharingTtl(
                            sourceIn,
                            sinkIn,
                            sourceOut,
                            sinkOut,
                            serviceAdapter),
                    MAX_DATA_SHARE_FILE_DESCRIPTORS_TTL_MS);
        }

        @Override
        public void reject() {
            Slog.i(TAG, "Data share request rejected by Content Capture service");
            logServiceEvent(CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__REJECT_DATA_SHARE_REQUEST);

            try {
                mClientAdapter.rejected();
            } catch (RemoteException e) {
                Slog.w(TAG, "Failed to call rejected() the client operation", e);
                try {
                    mClientAdapter.error(ContentCaptureManager.DATA_SHARE_ERROR_UNKNOWN);
                } catch (RemoteException e2) {
                    Slog.w(TAG, "Failed to call error() the client operation", e2);
                }
            }
        }

        private void enforceDataSharingTtl(ParcelFileDescriptor sourceIn,
                ParcelFileDescriptor sinkIn,
                ParcelFileDescriptor sourceOut,
                ParcelFileDescriptor sinkOut,
                IDataShareReadAdapter serviceAdapter) {

            synchronized (mParentService.mLock) {
                mParentService.mPackagesWithShareRequests
                        .remove(mDataShareRequest.getPackageName());

                // Interaction finished successfully <=> all data has been written to Content
                // Capture Service. If it hasn't been read successfully, service would be able
                // to signal by closing the input stream while not have finished reading.
                boolean finishedSuccessfully = !sinkIn.getFileDescriptor().valid()
                        && !sourceOut.getFileDescriptor().valid();

                if (finishedSuccessfully) {
                    if (!mLoggedWriteFinish.get()) {
                        logServiceEvent(EVENT__DATA_SHARE_WRITE_FINISHED);
                        mLoggedWriteFinish.set(true);
                    }
                    Slog.i(TAG, "Content capture data sharing session terminated "
                            + "successfully for package '"
                            + mDataShareRequest.getPackageName()
                            + "'");
                } else {
                    logServiceEvent(EVENT__DATA_SHARE_ERROR_TIMEOUT_INTERRUPTED);
                    Slog.i(TAG, "Reached the timeout of Content Capture data sharing session "
                            + "for package '"
                            + mDataShareRequest.getPackageName()
                            + "', terminating the pipe.");
                }

                // Ensure all the descriptors are closed after the session.
                bestEffortCloseFileDescriptors(sourceIn, sinkIn, sourceOut, sinkOut);

                if (!finishedSuccessfully) {
                    sendErrorSignal(mClientAdapter, serviceAdapter,
                            ContentCaptureManager.DATA_SHARE_ERROR_TIMEOUT_INTERRUPTED);
                }
            }
        }

        private Pair<ParcelFileDescriptor, ParcelFileDescriptor> createPipe() {
            ParcelFileDescriptor[] fileDescriptors;
            try {
                fileDescriptors = ParcelFileDescriptor.createPipe();
            } catch (IOException e) {
                Slog.e(TAG, "Failed to create a content capture data-sharing pipe", e);
                return null;
            }

            if (fileDescriptors.length != 2) {
                Slog.e(TAG, "Failed to create a content capture data-sharing pipe, "
                        + "unexpected number of file descriptors");
                return null;
            }

            if (!fileDescriptors[0].getFileDescriptor().valid()
                    || !fileDescriptors[1].getFileDescriptor().valid()) {
                Slog.e(TAG, "Failed to create a content capture data-sharing pipe, didn't "
                        + "receive a pair of valid file descriptors.");
                return null;
            }

            return Pair.create(fileDescriptors[0], fileDescriptors[1]);
        }

        private void bestEffortCloseFileDescriptor(ParcelFileDescriptor fd) {
            try {
                fd.close();
            } catch (IOException e) {
                Slog.e(TAG, "Failed to close a file descriptor", e);
            }
        }

        private void bestEffortCloseFileDescriptors(ParcelFileDescriptor... fds) {
            for (ParcelFileDescriptor fd : fds) {
                bestEffortCloseFileDescriptor(fd);
            }
        }

        private static void sendErrorSignal(
                IDataShareWriteAdapter clientAdapter,
                IDataShareReadAdapter serviceAdapter,
                int errorCode) {
            try {
                clientAdapter.error(errorCode);
            } catch (RemoteException e) {
                Slog.e(TAG, "Failed to call error() the client operation", e);
            }
            try {
                serviceAdapter.error(errorCode);
            } catch (RemoteException e) {
                Slog.e(TAG, "Failed to call error() the service operation", e);
            }
        }

        private void logServiceEvent(int eventType) {
            int userId = UserHandle.getCallingUserId();
            String serviceName = mParentService.mServiceNameResolver.getServiceName(userId);
            ContentCaptureMetricsLogger.writeServiceEvent(eventType, serviceName,
                    mDataShareRequest.getPackageName());
        }
    }
}
