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

Commit 20bdc46d authored by Chris Göllner's avatar Chris Göllner
Browse files

Create MediaProjectionTimestampStore to store last active session time

Backed by SharedPreferences, to persist the value across reboots.

Test: atest FrameworksServicesTests:MediaProjectionTimestampStoreTest
Bug: 304729462
Change-Id: I47cc428d7b73b06fa1cf773d01f88d39630d8795
parent 167941f3
Loading
Loading
Loading
Loading
+109 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.media.projection;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;

import java.io.File;
import java.time.Duration;
import java.time.Instant;
import java.time.InstantSource;

/** Stores timestamps of media projection sessions. */
public class MediaProjectionTimestampStore {
    private static final String PREFERENCES_FILE_NAME = "media_projection_timestamp";
    private static final String TIMESTAMP_PREF_KEY = "media_projection_timestamp_key";
    private static final Object sInstanceLock = new Object();

    @GuardedBy("sInstanceLock")
    private static MediaProjectionTimestampStore sInstance;

    private final Object mTimestampLock = new Object();

    @GuardedBy("mTimestampLock")
    private final SharedPreferences mSharedPreferences;

    private final InstantSource mInstantSource;

    @VisibleForTesting
    public MediaProjectionTimestampStore(
            SharedPreferences sharedPreferences, InstantSource instantSource) {
        this.mSharedPreferences = sharedPreferences;
        this.mInstantSource = instantSource;
    }

    /** Creates or returns an existing instance of {@link MediaProjectionTimestampStore}. */
    public static MediaProjectionTimestampStore getInstance(Context context) {
        synchronized (sInstanceLock) {
            if (sInstance == null) {
                File preferencesFile =
                        new File(Environment.getDataSystemDirectory(), PREFERENCES_FILE_NAME);
                SharedPreferences preferences =
                        context.getSharedPreferences(preferencesFile, Context.MODE_PRIVATE);
                sInstance = new MediaProjectionTimestampStore(preferences, InstantSource.system());
            }
            return sInstance;
        }
    }

    /**
     * Returns the time that has passed since the last active session, or {@code null} if there was
     * no last active session.
     */
    @Nullable
    public Duration timeSinceLastActiveSession() {
        synchronized (mTimestampLock) {
            Instant lastActiveSessionTimestamp = getLastActiveSessionTimestamp();
            if (lastActiveSessionTimestamp == null) {
                return null;
            }
            Instant now = mInstantSource.instant();
            return Duration.between(lastActiveSessionTimestamp, now);
        }
    }

    /** Registers that the current active session ended now. */
    public void registerActiveSessionEnded() {
        synchronized (mTimestampLock) {
            Instant now = mInstantSource.instant();
            setLastActiveSessionTimestamp(now);
        }
    }

    @GuardedBy("mTimestampLock")
    @Nullable
    private Instant getLastActiveSessionTimestamp() {
        long lastActiveSessionEpochMilli =
                mSharedPreferences.getLong(TIMESTAMP_PREF_KEY, /* defValue= */ -1);
        if (lastActiveSessionEpochMilli == -1) {
            return null;
        }
        return Instant.ofEpochMilli(lastActiveSessionEpochMilli);
    }

    @GuardedBy("mTimestampLock")
    private void setLastActiveSessionTimestamp(@NonNull Instant timestamp) {
        mSharedPreferences.edit().putLong(TIMESTAMP_PREF_KEY, timestamp.toEpochMilli()).apply();
    }
}
+104 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.media.projection;

import static com.google.common.truth.Truth.assertThat;

import android.content.Context;
import android.content.SharedPreferences;
import android.platform.test.annotations.Presubmit;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.File;
import java.time.Duration;
import java.time.Instant;
import java.time.InstantSource;

/**
 * Tests for the {@link MediaProjectionTimestampStore} class.
 *
 * <p>Build/Install/Run: atest FrameworksServicesTests:MediaProjectionTimestampStoreTest
 */
@SmallTest
@Presubmit
@RunWith(AndroidJUnit4.class)
public class MediaProjectionTimestampStoreTest {

    private static final String TEST_PREFS_FILE = "media-projection-timestamp-test";

    private final Context mContext =
            InstrumentationRegistry.getInstrumentation().getTargetContext();
    private final File mSharedPreferencesFile = new File(mContext.getCacheDir(), TEST_PREFS_FILE);
    private final SharedPreferences mSharedPreferences = createSharePreferences();

    private Instant mCurrentInstant = Instant.ofEpochMilli(0);

    private final InstantSource mInstantSource = () -> mCurrentInstant;
    private final MediaProjectionTimestampStore mStore =
            new MediaProjectionTimestampStore(mSharedPreferences, mInstantSource);

    @Before
    public void setUp() {
        mSharedPreferences.edit().clear().commit();
    }

    @After
    public void tearDown() {
        mSharedPreferences.edit().clear().commit();
        mSharedPreferencesFile.delete();
    }

    @Test
    public void timeSinceLastActiveSession_byDefault_returnsNull() {
        assertThat(mStore.timeSinceLastActiveSession()).isNull();
    }

    @Test
    public void timeSinceLastActiveSession_returnsBasedOnLastActiveSessionEnded() {
        mCurrentInstant = Instant.ofEpochMilli(0);
        mStore.registerActiveSessionEnded();

        mCurrentInstant = mCurrentInstant.plusSeconds(60);

        assertThat(mStore.timeSinceLastActiveSession()).isEqualTo(Duration.ofSeconds(60));
    }

    @Test
    public void timeSinceLastActiveSession_valueIsPersisted() {
        mCurrentInstant = Instant.ofEpochMilli(0);
        mStore.registerActiveSessionEnded();

        MediaProjectionTimestampStore newStoreInstance =
                new MediaProjectionTimestampStore(createSharePreferences(), mInstantSource);
        mCurrentInstant = mCurrentInstant.plusSeconds(123);

        assertThat(newStoreInstance.timeSinceLastActiveSession())
                .isEqualTo(Duration.ofSeconds(123));
    }

    private SharedPreferences createSharePreferences() {
        return mContext.getSharedPreferences(mSharedPreferencesFile, Context.MODE_PRIVATE);
    }
}