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

Commit 83677db3 authored by Aditya's avatar Aditya
Browse files

Add ConfigStore to use feature flags in DocsUI.

Created ConfigStore class to provide feature flags from device config to
be used in DocsUI. This also allows parametrizing the existing tests for
the private space feature flag.
Added check for S+ for using the feature flag in production as well as
testing.

Bug: 321894925
Test: atest unit tests and functional tests
Change-Id: I6cc8297d5a1dc5f0dfe850485320b38232f006fb
parent 77ee1ed2
Loading
Loading
Loading
Loading
+20 −4
Original line number Diff line number Diff line
@@ -74,7 +74,6 @@ import com.android.documentsui.roots.ProvidersCache;
import com.android.documentsui.sidebar.RootsFragment;
import com.android.documentsui.sorting.SortController;
import com.android.documentsui.sorting.SortModel;
import com.android.documentsui.util.FeatureFlagUtils;

import com.google.android.material.appbar.AppBarLayout;

@@ -104,6 +103,7 @@ public abstract class BaseActivity

    protected NavigationViewManager mNavigator;
    protected SortController mSortController;
    protected ConfigStore mConfigStore;

    private final List<EventListener> mEventListeners = new ArrayList<>();
    private final String mTag;
@@ -138,6 +138,16 @@ public abstract class BaseActivity

    public abstract Injector<?> getInjector();

    @VisibleForTesting
    protected void initConfigStore() {
        mConfigStore = DocumentsApplication.getConfigStore(this);
    }

    @VisibleForTesting
    public void setConfigStore(ConfigStore configStore) {
        mConfigStore = configStore;
    }

    @CallSuper
    @Override
    public void onCreate(Bundle savedInstanceState) {
@@ -159,6 +169,8 @@ public abstract class BaseActivity

        setContainer();

        initConfigStore();

        mInjector = getInjector();
        mState = getState(savedInstanceState);
        mDrawer = DrawerController.create(this, mInjector.config);
@@ -347,12 +359,14 @@ public abstract class BaseActivity

    private NavigationViewManager getNavigationViewManager(Breadcrumb breadcrumb,
            View profileTabsContainer) {
        if (FeatureFlagUtils.isPrivateSpaceEnabled()) {
        if (mConfigStore.isPrivateSpaceInDocsUIEnabled()) {
            return new NavigationViewManager(this, mDrawer, mState, this, breadcrumb,
                    profileTabsContainer, DocumentsApplication.getUserManagerState(this));
                    profileTabsContainer, DocumentsApplication.getUserManagerState(this),
                    mConfigStore);
        }
        return new NavigationViewManager(this, mDrawer, mState, this, breadcrumb,
                profileTabsContainer, DocumentsApplication.getUserIdManager(this));
                profileTabsContainer, DocumentsApplication.getUserIdManager(this),
                mConfigStore);
    }

    public void onPreferenceChanged(String pref) {
@@ -413,6 +427,7 @@ public abstract class BaseActivity
        mPreferencesMonitor.stop();
        mSortController.destroy();
        DocumentsApplication.invalidateUserManagerState(this);
        DocumentsApplication.invalidateConfigStore();
        super.onDestroy();
    }

@@ -438,6 +453,7 @@ public abstract class BaseActivity
                getApplicationContext()
                        .getResources()
                        .getBoolean(R.bool.show_hidden_files_by_default));
        state.configStore = mConfigStore;

        includeState(state);

+85 −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.documentsui;

import android.os.Binder;
import android.provider.DeviceConfig;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import com.android.modules.utils.build.SdkLevel;

import com.google.common.base.Supplier;

public interface ConfigStore {
    // TODO(b/288066342): Remove and replace after new constant definition in
    //  {@link android.provider.DeviceConfig}.
    String NAMESPACE_MEDIAPROVIDER = "mediaprovider";

    boolean DEFAULT_PICKER_PRIVATE_SPACE_ENABLED = false;

    /**
     * @return if the Private-Space-in-DocsUI is enabled
     */
    default boolean isPrivateSpaceInDocsUIEnabled() {
        return DEFAULT_PICKER_PRIVATE_SPACE_ENABLED;
    }

    /**
     * Implementation of the {@link ConfigStore} that reads "real" configs from
     * {@link android.provider.DeviceConfig}. Meant to be used by the "production" code.
     */
    class ConfigStoreImpl implements ConfigStore {
        @VisibleForTesting
        public static final String KEY_PRIVATE_SPACE_FEATURE_ENABLED =
                "private_space_feature_enabled";

        private static final boolean sCanReadDeviceConfig = SdkLevel.isAtLeastS();

        private Boolean mIsPrivateSpaceEnabled = null;

        @Override
        public boolean isPrivateSpaceInDocsUIEnabled() {
            if (mIsPrivateSpaceEnabled == null) {
                mIsPrivateSpaceEnabled = getBooleanDeviceConfig(
                        NAMESPACE_MEDIAPROVIDER,
                        KEY_PRIVATE_SPACE_FEATURE_ENABLED,
                        DEFAULT_PICKER_PRIVATE_SPACE_ENABLED);
            }
            return sCanReadDeviceConfig && mIsPrivateSpaceEnabled;
        }

        private static boolean getBooleanDeviceConfig(@NonNull String namespace,
                @NonNull String key, boolean defaultValue) {
            if (!sCanReadDeviceConfig) {
                return defaultValue;
            }
            return withCleanCallingIdentity(
                    () -> DeviceConfig.getBoolean(namespace, key, defaultValue));
        }

        private static <T> T withCleanCallingIdentity(@NonNull Supplier<T> action) {
            final long callingIdentity = Binder.clearCallingIdentity();
            try {
                return action.get();
            } finally {
                Binder.restoreCallingIdentity(callingIdentity);
            }
        }
    }
}
+1 −2
Original line number Diff line number Diff line
@@ -48,7 +48,6 @@ import com.android.documentsui.base.State;
import com.android.documentsui.base.UserId;
import com.android.documentsui.roots.RootCursorWrapper;
import com.android.documentsui.sorting.SortModel;
import com.android.documentsui.util.FeatureFlagUtils;

import java.util.ArrayList;
import java.util.List;
@@ -332,7 +331,7 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
    }

    private List<UserId> getUserIds() {
        if (FeatureFlagUtils.isPrivateSpaceEnabled()) {
        if (mState.configStore.isPrivateSpaceInDocsUIEnabled()) {
            return DocumentsApplication.getUserManagerState(getContext()).getUserIds();
        }
        return DocumentsApplication.getUserIdManager(getContext()).getUserIds();
+41 −6
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.os.RemoteException;
import android.text.format.DateUtils;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import com.android.documentsui.base.Lookup;
@@ -41,7 +42,6 @@ import com.android.documentsui.clipping.DocumentClipper;
import com.android.documentsui.queries.SearchHistoryManager;
import com.android.documentsui.roots.ProvidersCache;
import com.android.documentsui.theme.ThemeOverlayManager;
import com.android.documentsui.util.FeatureFlagUtils;
import com.android.modules.utils.build.SdkLevel;

import com.google.common.collect.Lists;
@@ -49,6 +49,8 @@ import com.google.common.collect.Lists;
import java.util.List;
import java.util.Objects;

import javax.annotation.concurrent.GuardedBy;

public class DocumentsApplication extends Application {
    private static final String TAG = "DocumentsApplication";
    private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
@@ -67,6 +69,10 @@ public class DocumentsApplication extends Application {
            Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE
    );

    @GuardedBy("DocumentsApplication.class")
    @Nullable
    private static volatile ConfigStore sConfigStore;

    private ProvidersCache mProviders;
    private ThumbnailCache mThumbnailCache;
    private ClipStorage mClipStore;
@@ -127,13 +133,33 @@ public class DocumentsApplication extends Application {
    }

    /**
     * Set mUserManagerState as null onDestroy of BaseActivity so that new session uses new instance
     * of mUserManagerState
     * Set {@link #mUserManagerState} as null onDestroy of BaseActivity so that new session uses new
     * instance of {@link #mUserManagerState}
     */
    public static void invalidateUserManagerState(Context context) {
        ((DocumentsApplication) context.getApplicationContext()).mUserManagerState = null;
    }

    /**
     * Retrieve {@link ConfigStore} instance to access feature flags in production code.
     */
    public static synchronized ConfigStore getConfigStore(Context context) {
        if (sConfigStore == null) {
            sConfigStore = new ConfigStore.ConfigStoreImpl();
        }
        return sConfigStore;
    }

    /**
     * Set {@link #sConfigStore} as null onDestroy of BaseActivity so that new session uses new
     * instance of {@link #sConfigStore}
     */
    public static void invalidateConfigStore() {
        synchronized (DocumentsApplication.class) {
            sConfigStore = null;
        }
    }

    private void onApplyOverlayFinish(boolean result) {
        Log.d(TAG, "OverlayManager.setEnabled() result: " + result);
    }
@@ -142,6 +168,11 @@ public class DocumentsApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        synchronized (DocumentsApplication.class) {
            if (sConfigStore == null) {
                sConfigStore = new ConfigStore.ConfigStoreImpl();
            }
        }

        final ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        final OverlayManager om = getSystemService(OverlayManager.class);
@@ -154,14 +185,18 @@ public class DocumentsApplication extends Application {
            Log.w(TAG, "Can't obtain OverlayManager from System Service!");
        }

        if (FeatureFlagUtils.isPrivateSpaceEnabled()) {
        if (getConfigStore(this).isPrivateSpaceInDocsUIEnabled()) {
            mUserManagerState = UserManagerState.create(this);
            mUserIdManager = null;
            mProviders = new ProvidersCache(this, mUserManagerState);
            synchronized (DocumentsApplication.class) {
                mProviders = new ProvidersCache(this, mUserManagerState, sConfigStore);
            }
        } else {
            mUserManagerState = null;
            mUserIdManager = UserIdManager.create(this);
            mProviders = new ProvidersCache(this, mUserIdManager);
            synchronized (DocumentsApplication.class) {
                mProviders = new ProvidersCache(this, mUserIdManager, sConfigStore);
            }
        }

        mProviders.updateAsync(/* forceRefreshAll= */ false, /* callback= */  null);
+1 −0
Original line number Diff line number Diff line
@@ -67,6 +67,7 @@ public final class HorizontalBreadcrumb extends RecyclerView implements Breadcru
        mLayoutManager = new HorizontalBreadcrumbLinearLayoutManager(
                getContext(), LinearLayoutManager.HORIZONTAL, false);
        mAdapter = new BreadcrumbAdapter(state, env, this::onKey);
        mAdapter.setHasStableIds(true);
        // Since we are using GestureDetector to detect click events, a11y services don't know which
        // views are clickable because we aren't using View.OnClickListener. Thus, we need to use a
        // custom accessibility delegate to route click events correctly.
Loading