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

Commit e30529d7 authored by Fabian Kozynski's avatar Fabian Kozynski
Browse files

Store state of active TileService

These tiles don't get polled on device startup or user change. Persist
their state (minus the icon) and retrieve when the CustomTile is
created.

Add an initialize method for QSTileImpl that gets handled in a
background thread.

Test: manual
Test: atest com.android.systemui.qs
Test: atest TileServiceTest ActiveTileServiceTest BooleanTileServiceTest
Fixes: 176789277
Change-Id: Id7775f4f2df56c25c13e2192d1bb824cb3b6a22a
parent 0245b5cb
Loading
Loading
Loading
Loading
+12 −1
Original line number Diff line number Diff line
@@ -45,7 +45,9 @@ import com.android.systemui.plugins.qs.QSFactory;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.qs.QSTileView;
import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.qs.external.CustomTileStatePersister;
import com.android.systemui.qs.external.TileLifecycleManager;
import com.android.systemui.qs.external.TileServiceKey;
import com.android.systemui.qs.external.TileServices;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.settings.UserTracker;
@@ -93,6 +95,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
    private final QSLogger mQSLogger;
    private final UiEventLogger mUiEventLogger;
    private final InstanceIdSequence mInstanceIdSequence;
    private final CustomTileStatePersister mCustomTileStatePersister;

    private final List<Callback> mCallbacks = new ArrayList<>();
    private AutoTileManager mAutoTiles;
@@ -119,7 +122,8 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
            QSLogger qsLogger,
            UiEventLogger uiEventLogger,
            UserTracker userTracker,
            SecureSettings secureSettings) {
            SecureSettings secureSettings,
            CustomTileStatePersister customTileStatePersister) {
        mIconController = iconController;
        mContext = context;
        mUserContext = context;
@@ -139,6 +143,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
        mDumpManager.registerDumpable(TAG, this);
        mUserTracker = userTracker;
        mSecureSettings = secureSettings;
        mCustomTileStatePersister = customTileStatePersister;

        mainHandler.post(() -> {
            // This is technically a hack to avoid circular dependency of
@@ -418,6 +423,11 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
        changeTiles(mTileSpecs, newSpecs);
    }

    /**
     * Change the tiles triggered by the user editing.
     * <p>
     * This is not called on device start, or on user change.
     */
    public void changeTiles(List<String> previousTiles, List<String> newTiles) {
        final List<String> copy = new ArrayList<>(previousTiles);
        final int NP = copy.size();
@@ -433,6 +443,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, D
                        mBroadcastDispatcher);
                lifecycleManager.onStopListening();
                lifecycleManager.onTileRemoved();
                mCustomTileStatePersister.removeState(new TileServiceKey(component, mCurrentUser));
                TileLifecycleManager.setTileAdded(mContext, component, false);
                lifecycleManager.flushMessagesAndUnbind();
            }
+64 −13
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@ import android.widget.Switch;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;

import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -85,6 +86,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
    private final IQSTileService mService;
    private final TileServiceManager mServiceManager;
    private final int mUser;
    private final CustomTileStatePersister mCustomTileStatePersister;
    private android.graphics.drawable.Icon mDefaultIcon;
    private CharSequence mDefaultLabel;

@@ -94,6 +96,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
    private boolean mIsTokenGranted;
    private boolean mIsShowingDialog;

    private final TileServiceKey mKey;

    private CustomTile(
            QSHost host,
            Looper backgroundLooper,
@@ -104,7 +108,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
            ActivityStarter activityStarter,
            QSLogger qsLogger,
            String action,
            Context userContext
            Context userContext,
            CustomTileStatePersister customTileStatePersister
    ) {
        super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                statusBarStateController, activityStarter, qsLogger);
@@ -113,15 +118,29 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
        mTile = new Tile();
        mUserContext = userContext;
        mUser = mUserContext.getUserId();
        updateDefaultTileAndIcon();
        mKey = new TileServiceKey(mComponent, mUser);

        mServiceManager = host.getTileServices().getTileWrapper(this);
        mService = mServiceManager.getTileService();
        mCustomTileStatePersister = customTileStatePersister;
    }

    @Override
    protected void handleInitialize() {
        updateDefaultTileAndIcon();
        if (mServiceManager.isToggleableTile()) {
            // Replace states with BooleanState
            resetStates();
        }

        mService = mServiceManager.getTileService();
        mServiceManager.setTileChangeListener(this);
        if (mServiceManager.isActiveTile()) {
            Tile t = mCustomTileStatePersister.readState(mKey);
            if (t != null) {
                applyTileState(t, /* overwriteNulls */ false);
                mServiceManager.clearPendingBind();
                refreshState();
            }
        }
    }

    @Override
@@ -191,7 +210,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener

    @Override
    public void onTileChanged(ComponentName tile) {
        updateDefaultTileAndIcon();
        mHandler.post(this::updateDefaultTileAndIcon);
    }

    @Override
@@ -213,16 +232,44 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
    }

    public Tile getQsTile() {
        // TODO(b/191145007) Move to background thread safely
        updateDefaultTileAndIcon();
        return mTile;
    }

    public void updateState(Tile tile) {
    /**
     * Update state of {@link this#mTile} from a remote {@link TileService}.
     * @param tile tile populated with state to apply
     */
    public void updateTileState(Tile tile) {
        // This comes from a binder call IQSService.updateQsTile
        mHandler.post(() -> handleUpdateTileState(tile));
    }

    private void handleUpdateTileState(Tile tile) {
        applyTileState(tile, /* overwriteNulls */ true);
        if (mServiceManager.isActiveTile()) {
            mCustomTileStatePersister.persistState(mKey, tile);
        }
    }

    @WorkerThread
    private void applyTileState(Tile tile, boolean overwriteNulls) {
        if (tile.getIcon() != null || overwriteNulls) {
            mTile.setIcon(tile.getIcon());
        }
        if (tile.getLabel() != null || overwriteNulls) {
            mTile.setLabel(tile.getLabel());
        }
        if (tile.getSubtitle() != null || overwriteNulls) {
            mTile.setSubtitle(tile.getSubtitle());
        }
        if (tile.getContentDescription() != null || overwriteNulls) {
            mTile.setContentDescription(tile.getContentDescription());
        }
        if (tile.getStateDescription() != null || overwriteNulls) {
            mTile.setStateDescription(tile.getStateDescription());
        }
        mTile.setState(tile.getState());
    }

@@ -459,6 +506,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
        final StatusBarStateController mStatusBarStateController;
        final ActivityStarter mActivityStarter;
        final QSLogger mQSLogger;
        final CustomTileStatePersister mCustomTileStatePersister;

        Context mUserContext;
        String mSpec = "";
@@ -472,7 +520,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
                MetricsLogger metricsLogger,
                StatusBarStateController statusBarStateController,
                ActivityStarter activityStarter,
                QSLogger qsLogger
                QSLogger qsLogger,
                CustomTileStatePersister customTileStatePersister
        ) {
            mQSHostLazy = hostLazy;
            mBackgroundLooper = backgroundLooper;
@@ -482,6 +531,7 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
            mStatusBarStateController = statusBarStateController;
            mActivityStarter = activityStarter;
            mQSLogger = qsLogger;
            mCustomTileStatePersister = customTileStatePersister;
        }

        Builder setSpec(@NonNull String spec) {
@@ -509,7 +559,8 @@ public class CustomTile extends QSTileImpl<State> implements TileChangeListener
                    mActivityStarter,
                    mQSLogger,
                    action,
                    mUserContext
                    mUserContext,
                    mCustomTileStatePersister
            );
        }
    }
+122 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.systemui.qs.external

import android.content.ComponentName
import android.content.Context
import android.service.quicksettings.Tile
import android.util.Log
import com.android.internal.annotations.VisibleForTesting
import org.json.JSONException
import org.json.JSONObject
import javax.inject.Inject

data class TileServiceKey(val componentName: ComponentName, val user: Int) {
    private val string = "${componentName.flattenToString()}:$user"
    override fun toString() = string
}
private const val STATE = "state"
private const val LABEL = "label"
private const val SUBTITLE = "subtitle"
private const val CONTENT_DESCRIPTION = "content_description"
private const val STATE_DESCRIPTION = "state_description"

/**
 * Persists and retrieves state for [CustomTile].
 *
 * This class will persists to a fixed [SharedPreference] file a state for a pair of [ComponentName]
 * and user id ([TileServiceKey]).
 *
 * It persists the state from a [Tile] necessary to present the view in the same state when
 * retrieved, with the exception of the icon.
 */
class CustomTileStatePersister @Inject constructor(context: Context) {
    companion object {
        private const val FILE_NAME = "custom_tiles_state"
    }

    private val sharedPreferences = context.getSharedPreferences(FILE_NAME, 0)

    /**
     * Read the state from [SharedPreferences].
     *
     * Returns `null` if the tile has no saved state.
     *
     * Any fields that have not been saved will be set to `null`
     */
    fun readState(key: TileServiceKey): Tile? {
        val state = sharedPreferences.getString(key.toString(), null) ?: return null
        return try {
            readTileFromString(state)
        } catch (e: JSONException) {
            Log.e("TileServicePersistence", "Bad saved state: $state", e)
            null
        }
    }

    /**
     * Persists the state into [SharedPreferences].
     *
     * The implementation does not store fields that are `null` or icons.
     */
    fun persistState(key: TileServiceKey, tile: Tile) {
        val state = writeToString(tile)

        sharedPreferences.edit().putString(key.toString(), state).apply()
    }

    /**
     * Removes the state for a given tile, user pair.
     *
     * Used when the tile is removed by the user.
     */
    fun removeState(key: TileServiceKey) {
        sharedPreferences.edit().remove(key.toString()).apply()
    }
}

@VisibleForTesting
internal fun readTileFromString(stateString: String): Tile {
    val json = JSONObject(stateString)
    return Tile().apply {
        state = json.getInt(STATE)
        label = json.getStringOrNull(LABEL)
        subtitle = json.getStringOrNull(SUBTITLE)
        contentDescription = json.getStringOrNull(CONTENT_DESCRIPTION)
        stateDescription = json.getStringOrNull(STATE_DESCRIPTION)
    }
}

// Properties with null values will not be saved to the Json string in any way. This makes sure
// to properly retrieve a null in that case.
private fun JSONObject.getStringOrNull(name: String): String? {
    return if (has(name)) getString(name) else null
}

@VisibleForTesting
internal fun writeToString(tile: Tile): String {
    // Not storing the icon
    return with(tile) {
        JSONObject()
            .put(STATE, state)
            .put(LABEL, label)
            .put(SUBTITLE, subtitle)
            .put(CONTENT_DESCRIPTION, contentDescription)
            .put(STATE_DESCRIPTION, stateDescription)
            .toString()
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -204,7 +204,7 @@ public class TileServices extends IQSService.Stub {
                tileServiceManager.clearPendingBind();
                tileServiceManager.setLastUpdate(System.currentTimeMillis());
            }
            customTile.updateState(tile);
            customTile.updateTileState(tile);
            customTile.refreshState();
        }
    }
+2 −1
Original line number Diff line number Diff line
@@ -160,7 +160,8 @@ public class QSFactoryImpl implements QSFactory {
    public QSTile createTile(String tileSpec) {
        QSTileImpl tile = createTileInternal(tileSpec);
        if (tile != null) {
            tile.handleStale(); // Tile was just created, must be stale.
            tile.initialize();
            tile.postStale(); // Tile was just created, must be stale.
        }
        return tile;
    }
Loading