Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8e583ef5 authored by Wenhao Wang's avatar Wenhao Wang Committed by Android (Google) Code Review
Browse files

Merge "[Forensic] Add DataAggregator" into main

parents 5c32317a 54708ada
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -61,6 +61,14 @@ public final class ForensicEvent implements Parcelable {
        in.readMap(mKeyValuePairs, getClass().getClassLoader(), String.class, String.class);
    }

    public String getType() {
        return mType;
    }

    public Map<String, String> getKeyValuePairs() {
        return mKeyValuePairs;
    }

    @Override
    public void writeToParcel(@NonNull Parcel out, int flags) {
        out.writeString(mType);
+142 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.security.forensic;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.security.forensic.ForensicEvent;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.ServiceThread;

import java.util.ArrayList;
import java.util.List;

public class DataAggregator {
    private static final String TAG = "Forensic DataAggregator";
    private static final int MSG_SINGLE_DATA = 0;
    private static final int MSG_BATCH_DATA = 1;
    private static final int MSG_DISABLE = 2;

    private static final int STORED_EVENTS_SIZE_LIMIT = 1024;
    private final ForensicService mForensicService;
    private final ArrayList<DataSource> mDataSources;

    private List<ForensicEvent> mStoredEvents = new ArrayList<>();
    private ServiceThread mHandlerThread;
    private Handler mHandler;
    public DataAggregator(ForensicService forensicService) {
        mForensicService = forensicService;
        mDataSources = new ArrayList<DataSource>();
    }

    @VisibleForTesting
    void setHandler(Looper looper, ServiceThread serviceThread) {
        mHandlerThread = serviceThread;
        mHandler = new EventHandler(looper, this);
    }

    /**
     * Initialize DataSources
     * @return Whether the initialization succeeds.
     */
    // TODO: Add the corresponding data sources
    public boolean initialize() {
        return true;
    }

    /**
     * Enable the data collection of all DataSources.
     */
    public void enable() {
        mHandlerThread = new ServiceThread(TAG, android.os.Process.THREAD_PRIORITY_BACKGROUND,
                /* allowIo */ false);
        mHandlerThread.start();
        mHandler = new EventHandler(mHandlerThread.getLooper(), this);
        for (DataSource ds : mDataSources) {
            ds.enable();
        }
    }

    /**
     * DataSource calls it to transmit a single event.
     */
    public void addSingleData(ForensicEvent event) {
        mHandler.obtainMessage(MSG_SINGLE_DATA, event).sendToTarget();
    }

    /**
     * DataSource calls it to transmit list of events.
     */
    public void addBatchData(List<ForensicEvent> events) {
        mHandler.obtainMessage(MSG_BATCH_DATA, events).sendToTarget();
    }

    /**
     * Disable the data collection of all DataSources.
     */
    public void disable() {
        mHandler.obtainMessage(MSG_DISABLE).sendToTarget();
    }

    private void onNewSingleData(ForensicEvent event) {
        if (mStoredEvents.size() < STORED_EVENTS_SIZE_LIMIT) {
            mStoredEvents.add(event);
        } else {
            mForensicService.addNewData(mStoredEvents);
            mStoredEvents = new ArrayList<>();
        }
    }

    private void onNewBatchData(List<ForensicEvent> events) {
        mForensicService.addNewData(events);
    }

    private void onDisable() {
        for (DataSource ds : mDataSources) {
            ds.disable();
        }
        mHandlerThread.quitSafely();
        mHandlerThread = null;
    }

    private static class EventHandler extends Handler {
        private final DataAggregator mDataAggregator;
        EventHandler(Looper looper, DataAggregator dataAggregator) {
            super(looper);
            mDataAggregator = dataAggregator;
        }
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_SINGLE_DATA:
                    mDataAggregator.onNewSingleData((ForensicEvent) msg.obj);
                    break;
                case MSG_BATCH_DATA:
                    mDataAggregator.onNewBatchData((List<ForensicEvent>) msg.obj);
                    break;
                case MSG_DISABLE:
                    mDataAggregator.onDisable();
                    break;
                default:
                    Slog.w(TAG, "Unknown message: " + msg.what);
            }
        }
    }
}
+29 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.security.forensic;

public interface DataSource {
    /**
     * Enable the data collection.
     */
    void enable();

    /**
     * Disable the data collection.
     */
    void disable();
}
+31 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.security.forensic.ForensicEvent;
import android.security.forensic.IForensicService;
import android.security.forensic.IForensicServiceCommandCallback;
import android.security.forensic.IForensicServiceStateCallback;
@@ -32,6 +33,7 @@ import com.android.server.ServiceThread;
import com.android.server.SystemService;

import java.util.ArrayList;
import java.util.List;

/**
 * @hide
@@ -64,6 +66,7 @@ public class ForensicService extends SystemService {
    private final Context mContext;
    private final Handler mHandler;
    private final BackupTransportConnection mBackupTransportConnection;
    private final DataAggregator mDataAggregator;
    private final BinderService mBinderService;

    private final ArrayList<IForensicServiceStateCallback> mStateMonitors = new ArrayList<>();
@@ -79,6 +82,7 @@ public class ForensicService extends SystemService {
        mContext = injector.getContext();
        mHandler = new EventHandler(injector.getLooper(), this);
        mBackupTransportConnection = injector.getBackupTransportConnection();
        mDataAggregator = injector.getDataAggregator(this);
        mBinderService = new BinderService(this);
    }

@@ -167,6 +171,9 @@ public class ForensicService extends SystemService {
                        Slog.e(TAG, "RemoteException", e);
                    }
                    break;
                case MSG_BACKUP:
                    mService.backup((List<ForensicEvent>) msg.obj);
                    break;
                default:
                    Slog.w(TAG, "Unknown message: " + msg.what);
            }
@@ -192,6 +199,10 @@ public class ForensicService extends SystemService {
    private void makeVisible(IForensicServiceCommandCallback callback) throws RemoteException {
        switch (mState) {
            case STATE_INVISIBLE:
                if (!mDataAggregator.initialize()) {
                    callback.onFailure(ERROR_DATA_SOURCE_UNAVAILABLE);
                    break;
                }
                mState = STATE_VISIBLE;
                notifyStateMonitors();
                callback.onSuccess();
@@ -227,6 +238,7 @@ public class ForensicService extends SystemService {
                    callback.onFailure(ERROR_BACKUP_TRANSPORT_UNAVAILABLE);
                    break;
                }
                mDataAggregator.enable();
                mState = STATE_ENABLED;
                notifyStateMonitors();
                callback.onSuccess();
@@ -243,6 +255,7 @@ public class ForensicService extends SystemService {
        switch (mState) {
            case STATE_ENABLED:
                mBackupTransportConnection.release();
                mDataAggregator.disable();
                mState = STATE_VISIBLE;
                notifyStateMonitors();
                callback.onSuccess();
@@ -255,6 +268,17 @@ public class ForensicService extends SystemService {
        }
    }

    /**
     * Add a list of ForensicEvent.
     */
    public void addNewData(List<ForensicEvent> events) {
        mHandler.obtainMessage(MSG_BACKUP, events).sendToTarget();
    }

    private void backup(List<ForensicEvent> events) {
        mBackupTransportConnection.addData(events);
    }

    @Override
    public void onStart() {
        try {
@@ -275,6 +299,8 @@ public class ForensicService extends SystemService {
        Looper getLooper();

        BackupTransportConnection getBackupTransportConnection();

        DataAggregator getDataAggregator(ForensicService forensicService);
    }

    private static final class InjectorImpl implements Injector {
@@ -303,6 +329,11 @@ public class ForensicService extends SystemService {
        public BackupTransportConnection getBackupTransportConnection() {
            return new BackupTransportConnection(mContext);
        }

        @Override
        public DataAggregator getDataAggregator(ForensicService forensicService) {
            return new DataAggregator(forensicService);
        }
    }
}
+102 −1
Original line number Diff line number Diff line
@@ -19,23 +19,35 @@ package com.android.server.security.forensic;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Looper;
import android.os.RemoteException;
import android.os.test.TestLooper;
import android.security.forensic.ForensicEvent;
import android.security.forensic.IForensicServiceCommandCallback;
import android.security.forensic.IForensicServiceStateCallback;
import android.util.ArrayMap;

import com.android.server.ServiceThread;

import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class ForensicServiceTest {
    private static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN;
    private static final int STATE_INVISIBLE = IForensicServiceStateCallback.State.INVISIBLE;
@@ -55,10 +67,12 @@ public class ForensicServiceTest {
    @Mock
    private Context mContext;
    private BackupTransportConnection mBackupTransportConnection;

    private DataAggregator mDataAggregator;
    private ForensicService mForensicService;
    private TestLooper mTestLooper;
    private Looper mLooper;
    private TestLooper mTestLooperOfDataAggregator;
    private Looper mLooperOfDataAggregator;

    @SuppressLint("VisibleForTests")
    @Before
@@ -67,6 +81,8 @@ public class ForensicServiceTest {

        mTestLooper = new TestLooper();
        mLooper = mTestLooper.getLooper();
        mTestLooperOfDataAggregator = new TestLooper();
        mLooperOfDataAggregator = mTestLooperOfDataAggregator.getLooper();
        mForensicService = new ForensicService(new MockInjector(mContext));
        mForensicService.onStart();
    }
@@ -121,6 +137,8 @@ public class ForensicServiceTest {
        assertEquals(STATE_INVISIBLE, scb1.mState);
        assertEquals(STATE_INVISIBLE, scb2.mState);

        doReturn(true).when(mDataAggregator).initialize();

        CommandCallback ccb = new CommandCallback();
        mForensicService.getBinderService().makeVisible(ccb);
        mTestLooper.dispatchAll();
@@ -129,6 +147,29 @@ public class ForensicServiceTest {
        assertNull(ccb.mErrorCode);
    }

    @Test
    public void testMakeVisible_FromInvisible_TwoMonitors_DataSourceUnavailable()
            throws RemoteException {
        mForensicService.setState(STATE_INVISIBLE);
        StateCallback scb1 = new StateCallback();
        StateCallback scb2 = new StateCallback();
        mForensicService.getBinderService().monitorState(scb1);
        mForensicService.getBinderService().monitorState(scb2);
        mTestLooper.dispatchAll();
        assertEquals(STATE_INVISIBLE, scb1.mState);
        assertEquals(STATE_INVISIBLE, scb2.mState);

        doReturn(false).when(mDataAggregator).initialize();

        CommandCallback ccb = new CommandCallback();
        mForensicService.getBinderService().makeVisible(ccb);
        mTestLooper.dispatchAll();
        assertEquals(STATE_INVISIBLE, scb1.mState);
        assertEquals(STATE_INVISIBLE, scb2.mState);
        assertNotNull(ccb.mErrorCode);
        assertEquals(ERROR_DATA_SOURCE_UNAVAILABLE, ccb.mErrorCode.intValue());
    }

    @Test
    public void testMakeVisible_FromVisible_TwoMonitors() throws RemoteException {
        mForensicService.setState(STATE_VISIBLE);
@@ -262,6 +303,8 @@ public class ForensicServiceTest {
        CommandCallback ccb = new CommandCallback();
        mForensicService.getBinderService().enable(ccb);
        mTestLooper.dispatchAll();

        verify(mDataAggregator, times(1)).enable();
        assertEquals(STATE_ENABLED, scb1.mState);
        assertEquals(STATE_ENABLED, scb2.mState);
        assertNull(ccb.mErrorCode);
@@ -361,14 +404,67 @@ public class ForensicServiceTest {

        doNothing().when(mBackupTransportConnection).release();

        ServiceThread mockThread = spy(ServiceThread.class);
        mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread);

        CommandCallback ccb = new CommandCallback();
        mForensicService.getBinderService().disable(ccb);
        mTestLooper.dispatchAll();
        mTestLooperOfDataAggregator.dispatchAll();
        // TODO: We can verify the data sources once we implement them.
        verify(mockThread, times(1)).quitSafely();
        assertEquals(STATE_VISIBLE, scb1.mState);
        assertEquals(STATE_VISIBLE, scb2.mState);
        assertNull(ccb.mErrorCode);
    }

    @Test
    public void testDataAggregator_AddBatchData() {
        mForensicService.setState(STATE_ENABLED);
        ServiceThread mockThread = spy(ServiceThread.class);
        mDataAggregator.setHandler(mLooperOfDataAggregator, mockThread);

        String eventOneType = "event_one_type";
        String eventOneMapKey = "event_one_map_key";
        String eventOneMapVal = "event_one_map_val";
        Map<String, String> eventOneMap = new ArrayMap<String, String>();
        eventOneMap.put(eventOneMapKey, eventOneMapVal);
        ForensicEvent eventOne = new ForensicEvent(eventOneType, eventOneMap);

        String eventTwoType = "event_two_type";
        String eventTwoMapKey = "event_two_map_key";
        String eventTwoMapVal = "event_two_map_val";
        Map<String, String> eventTwoMap = new ArrayMap<String, String>();
        eventTwoMap.put(eventTwoMapKey, eventTwoMapVal);
        ForensicEvent eventTwo = new ForensicEvent(eventTwoType, eventTwoMap);

        List<ForensicEvent> events = new ArrayList<>();
        events.add(eventOne);
        events.add(eventTwo);

        doReturn(true).when(mBackupTransportConnection).addData(any());

        mDataAggregator.addBatchData(events);
        mTestLooperOfDataAggregator.dispatchAll();
        mTestLooper.dispatchAll();

        ArgumentCaptor<List<ForensicEvent>> captor = ArgumentCaptor.forClass(List.class);
        verify(mBackupTransportConnection).addData(captor.capture());
        List<ForensicEvent> receivedEvents = captor.getValue();
        assertEquals(receivedEvents.size(), 2);

        assertEquals(receivedEvents.getFirst().getType(), eventOneType);
        assertEquals(receivedEvents.getFirst().getKeyValuePairs().size(), 1);
        assertEquals(receivedEvents.getFirst().getKeyValuePairs().get(eventOneMapKey),
                eventOneMapVal);

        assertEquals(receivedEvents.getLast().getType(), eventTwoType);
        assertEquals(receivedEvents.getLast().getKeyValuePairs().size(), 1);
        assertEquals(receivedEvents.getLast().getKeyValuePairs().get(eventTwoMapKey),
                eventTwoMapVal);

    }

    private class MockInjector implements ForensicService.Injector {
        private final Context mContext;

@@ -393,6 +489,11 @@ public class ForensicServiceTest {
            return mBackupTransportConnection;
        }

        @Override
        public DataAggregator getDataAggregator(ForensicService forensicService) {
            mDataAggregator = spy(new DataAggregator(forensicService));
            return mDataAggregator;
        }
    }

    private static class StateCallback extends IForensicServiceStateCallback.Stub {