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

Commit 22904a05 authored by jackqdyulei's avatar jackqdyulei
Browse files

Add UiBlocker for DashboardFragment

If DashboardFragment detects blocker controllers, it won't display
until:
1. All blocking controllers finish bg works.
2. Timeout

This CL adds UiBlockerController to control this behavior and
BlockingSlicePreferenceController as an example.

Bug: 120803703
Test: atest SettingsUnitTests
Change-Id: I66fc194776d46ee49b3ec7685f3167834e673ba2
parent 0057e45e
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -31,7 +31,7 @@

    <com.android.settings.slices.SlicePreference
        android:key="bt_device_slice"
        settings:controller="com.android.settings.slices.SlicePreferenceController"
        settings:controller="com.android.settings.slices.BlockingSlicePrefController"
        settings:allowDividerBelow="true"
        settings:allowDividerAbove="true"/>

+2 −2
Original line number Diff line number Diff line
@@ -30,7 +30,7 @@ import com.android.settings.R;
import com.android.settings.core.FeatureFlags;
import com.android.settings.dashboard.RestrictedDashboardFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.slices.SlicePreferenceController;
import com.android.settings.slices.BlockingSlicePrefController;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import com.android.settingslib.core.AbstractPreferenceController;
@@ -106,7 +106,7 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
        if (FeatureFlagUtils.isEnabled(context, FeatureFlags.SLICE_INJECTION)) {
            final BluetoothFeatureProvider featureProvider = FeatureFactory.getFactory(context)
                    .getBluetoothFeatureProvider(context);
            use(SlicePreferenceController.class).setSliceUri(
            use(BlockingSlicePrefController.class).setSliceUri(
                    featureProvider.getBluetoothDeviceSettingsUri(mDeviceAddress));
        }
    }
+33 −0
Original line number Diff line number Diff line
@@ -106,6 +106,7 @@ public abstract class BasePreferenceController extends AbstractPreferenceControl


    protected final String mPreferenceKey;
    protected UiBlockListener mUiBlockListener;

    /**
     * Instantiate a controller as specified controller type and user-defined key.
@@ -289,4 +290,36 @@ public abstract class BasePreferenceController extends AbstractPreferenceControl
     */
    public void updateRawDataToIndex(List<SearchIndexableRaw> rawData) {
    }

    /**
     * Set {@link UiBlockListener}
     * @param uiBlockListener listener to set
     */
    public void setUiBlockListener(UiBlockListener uiBlockListener) {
        mUiBlockListener = uiBlockListener;
    }

    /**
     * Listener to invoke when background job is finished
     */
    public interface UiBlockListener {
        /**
         * To notify client that UI related background work is finished.
         * (i.e. Slice is fully loaded.)
         * @param controller Controller that contains background work
         */
        void onBlockerWorkFinished(BasePreferenceController controller);
    }

    /**
     * Used for {@link BasePreferenceController} to decide whether it is ui blocker.
     * If it is, entire UI will be invisible for a certain period until controller
     * invokes {@link UiBlockListener}
     *
     * This won't block UI thread however has similar side effect. Please use it if you
     * want to avoid janky animation(i.e. new preference is added in the middle of page).
     *
     * This music be used in {@link BasePreferenceController}
     */
    public interface UiBlocker {}
}
 No newline at end of file
+48 −3
Original line number Diff line number Diff line
@@ -56,7 +56,8 @@ import java.util.Set;
 */
public abstract class DashboardFragment extends SettingsPreferenceFragment
        implements SettingsBaseActivity.CategoryListener, Indexable,
        SummaryLoader.SummaryConsumer, PreferenceGroup.OnExpandButtonClickListener {
        SummaryLoader.SummaryConsumer, PreferenceGroup.OnExpandButtonClickListener,
        BasePreferenceController.UiBlockListener {
    private static final String TAG = "DashboardFragment";

    private final Map<Class, List<AbstractPreferenceController>> mPreferenceControllers =
@@ -67,6 +68,7 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
    private DashboardTilePlaceholderPreferenceController mPlaceholderPreferenceController;
    private boolean mListeningToCategoryChange;
    private SummaryLoader mSummaryLoader;
    private UiBlockerController mBlockerController;

    @Override
    public void onAttach(Context context) {
@@ -105,6 +107,22 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
        for (AbstractPreferenceController controller : controllers) {
            addPreferenceController(controller);
        }

        checkUiBlocker(controllers);
    }

    private void checkUiBlocker(List<AbstractPreferenceController> controllers) {
        final List<String> keys = new ArrayList<>();
        controllers
                .stream()
                .filter(controller -> controller instanceof BasePreferenceController.UiBlocker)
                .forEach(controller -> {
                    ((BasePreferenceController) controller).setUiBlockListener(this);
                    keys.add(controller.getPreferenceKey());
                });

        mBlockerController = new UiBlockerController(keys);
        mBlockerController.start(()->updatePreferenceVisibility());
    }

    @Override
@@ -319,10 +337,11 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
     * DashboardCategory.
     */
    private void refreshAllPreferences(final String TAG) {
        final PreferenceScreen screen = getPreferenceScreen();
        // First remove old preferences.
        if (getPreferenceScreen() != null) {
        if (screen != null) {
            // Intentionally do not cache PreferenceScreen because it will be recreated later.
            getPreferenceScreen().removeAll();
            screen.removeAll();
        }

        // Add resource based tiles.
@@ -335,6 +354,27 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
            Log.d(TAG, "All preferences added, reporting fully drawn");
            activity.reportFullyDrawn();
        }

        updatePreferenceVisibility();
    }

    private void updatePreferenceVisibility() {
        final PreferenceScreen screen = getPreferenceScreen();
        if (screen == null) {
            return;
        }

        final boolean visible = mBlockerController.isBlockerFinished();
        for (List<AbstractPreferenceController> controllerList :
                mPreferenceControllers.values()) {
            for (AbstractPreferenceController controller : controllerList) {
                final String key = controller.getPreferenceKey();
                final Preference preference = screen.findPreference(key);
                if (preference != null) {
                    preference.setVisible(visible && controller.isAvailable());
                }
            }
        }
    }

    /**
@@ -413,4 +453,9 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
        }
        mSummaryLoader.setListening(true);
    }

    @Override
    public void onBlockerWorkFinished(BasePreferenceController controller) {
        mBlockerController.countDown(controller.getPreferenceKey());
    }
}
+101 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.settings.dashboard;

import android.util.Log;

import androidx.annotation.NonNull;

import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.utils.ThreadUtils;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * Control ui blocker data and check whether it is finished
 *
 * @see BasePreferenceController.UiBlocker
 * @see BasePreferenceController.OnUiBlockListener
 */
public class UiBlockerController {
    private static final String TAG = "UiBlockerController";
    private static final int TIMEOUT_MILLIS = 500;

    private CountDownLatch mCountDownLatch;
    private boolean mBlockerFinished;
    private Set<String> mKeys;
    private long mTimeoutMillis;

    public UiBlockerController(@NonNull List<String> keys) {
        this(keys, TIMEOUT_MILLIS);
    }

    public UiBlockerController(@NonNull List<String> keys, long timeout) {
        mCountDownLatch = new CountDownLatch(keys.size());
        mBlockerFinished = keys.isEmpty();
        mKeys = new HashSet<>(keys);
        mTimeoutMillis = timeout;
    }

    /**
     * Start background thread, it will invoke {@code finishRunnable} if any condition is met
     *
     * 1. Waiting time exceeds {@link #mTimeoutMillis}
     * 2. All background work that associated with {@link #mCountDownLatch} is finished
     */
    public boolean start(Runnable finishRunnable) {
        if (mKeys.isEmpty()) {
            // Don't need to run finishRunnable because it doesn't start
            return false;
        }
        ThreadUtils.postOnBackgroundThread(() -> {
            try {
                mCountDownLatch.await(mTimeoutMillis, TimeUnit.MILLISECONDS);
            } catch (InterruptedException e) {
                Log.w(TAG, "interrupted");
            }
            mBlockerFinished = true;
            ThreadUtils.postOnMainThread(finishRunnable);
        });

        return true;
    }

    /**
     * Return {@code true} if all work finished
     */
    public boolean isBlockerFinished() {
        return mBlockerFinished;
    }

    /**
     * Count down latch by {@code key}. It only count down 1 time if same key count down multiple
     * times.
     */
    public boolean countDown(String key) {
        if (mKeys.remove(key)) {
            mCountDownLatch.countDown();
            return true;
        }

        return false;
    }
}
Loading