/* * Copyright (C) 2016 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.customize; import android.Manifest.permission; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; import android.os.Build; import android.provider.Settings; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; import android.text.TextUtils; import android.util.ArraySet; import android.widget.Button; import androidx.annotation.Nullable; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon; import com.android.systemui.settings.UserTracker; import com.android.systemui.util.leak.GarbageMonitor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.concurrent.Executor; import javax.inject.Inject; /** */ @QSScope public class TileQueryHelper { private static final String TAG = "TileQueryHelper"; private final ArrayList mTiles = new ArrayList<>(); private final ArraySet mSpecs = new ArraySet<>(); private final Executor mMainExecutor; private final Executor mBgExecutor; private final Context mContext; private final UserTracker mUserTracker; private TileStateListener mListener; private boolean mFinished; @Inject public TileQueryHelper( Context context, UserTracker userTracker, @Main Executor mainExecutor, @Background Executor bgExecutor ) { mContext = context; mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mUserTracker = userTracker; } public void setListener(@Nullable TileStateListener listener) { mListener = listener; } public void queryTiles(QSHost host) { mTiles.clear(); mSpecs.clear(); mFinished = false; // Enqueue jobs to fetch every system tile and then ever package tile. addCurrentAndStockTiles(host); } public boolean isFinished() { return mFinished; } private void addCurrentAndStockTiles(QSHost host) { String stock = mContext.getString(R.string.quick_settings_tiles_stock); String current = Settings.Secure.getString(mContext.getContentResolver(), Settings.Secure.QS_TILES); final ArrayList possibleTiles = new ArrayList<>(); if (current != null) { // The setting QS_TILES is not populated immediately upon Factory Reset possibleTiles.addAll(Arrays.asList(current.split(","))); } else { current = ""; } String[] stockSplit = stock.split(","); for (String spec : stockSplit) { if (!current.contains(spec)) { possibleTiles.add(spec); } } if (Build.IS_DEBUGGABLE && !current.contains(GarbageMonitor.MemoryTile.TILE_SPEC)) { possibleTiles.add(GarbageMonitor.MemoryTile.TILE_SPEC); } final ArrayList tilesToAdd = new ArrayList<>(); possibleTiles.remove("cell"); possibleTiles.remove("wifi"); for (String spec : possibleTiles) { // Only add current and stock tiles that can be created from QSFactoryImpl. // Do not include CustomTile. Those will be created by `addPackageTiles`. if (spec.startsWith(CustomTile.PREFIX)) continue; final QSTile tile = host.createTile(spec); if (tile == null) { continue; } else if (!tile.isAvailable()) { tile.setTileSpec(spec); tile.destroy(); continue; } tile.setTileSpec(spec); tilesToAdd.add(tile); } new TileCollector(tilesToAdd, host).startListening(); } private static class TilePair { private TilePair(QSTile tile) { mTile = tile; } QSTile mTile; boolean mReady = false; } private class TileCollector implements QSTile.Callback { private final List mQSTileList = new ArrayList<>(); private final QSHost mQSHost; TileCollector(List tilesToAdd, QSHost host) { for (QSTile tile: tilesToAdd) { TilePair pair = new TilePair(tile); mQSTileList.add(pair); } mQSHost = host; if (tilesToAdd.isEmpty()) { mBgExecutor.execute(this::finished); } } private void finished() { notifyTilesChanged(false); addPackageTiles(mQSHost); } private void startListening() { for (TilePair pair: mQSTileList) { pair.mTile.addCallback(this); pair.mTile.setListening(this, true); // Make sure that at least one refresh state happens pair.mTile.refreshState(); } } // This is called in the Bg thread @Override public void onStateChanged(State s) { boolean allReady = true; for (TilePair pair: mQSTileList) { if (!pair.mReady && pair.mTile.isTileReady()) { pair.mTile.removeCallback(this); pair.mTile.setListening(this, false); pair.mReady = true; } else if (!pair.mReady) { allReady = false; } } if (allReady) { for (TilePair pair : mQSTileList) { QSTile tile = pair.mTile; final QSTile.State state = tile.getState().copy(); // Ignore the current state and get the generic label instead. state.label = tile.getTileLabel(); tile.destroy(); addTile(tile.getTileSpec(), null, state, true); } finished(); } } } private void addPackageTiles(final QSHost host) { mBgExecutor.execute(() -> { Collection params = host.getTiles(); PackageManager pm = mContext.getPackageManager(); List services = pm.queryIntentServicesAsUser( new Intent(TileService.ACTION_QS_TILE), 0, mUserTracker.getUserId()); String stockTiles = mContext.getString(R.string.quick_settings_tiles_stock); for (ResolveInfo info : services) { String packageName = info.serviceInfo.packageName; ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name); // Don't include apps that are a part of the default tile set. if (stockTiles.contains(componentName.flattenToString())) { continue; } final CharSequence appLabel = info.serviceInfo.applicationInfo.loadLabel(pm); String spec = CustomTile.toSpec(componentName); State state = getState(params, spec); if (state != null) { addTile(spec, appLabel, state, false); continue; } if (info.serviceInfo.icon == 0 && info.serviceInfo.applicationInfo.icon == 0) { continue; } Drawable icon = info.serviceInfo.loadIcon(pm); if (!permission.BIND_QUICK_SETTINGS_TILE.equals(info.serviceInfo.permission)) { continue; } if (icon == null) { continue; } icon.mutate(); icon.setTint(mContext.getColor(android.R.color.white)); CharSequence label = info.serviceInfo.loadLabel(pm); createStateAndAddTile(spec, icon, label != null ? label.toString() : "null", appLabel); } notifyTilesChanged(true); }); } private void notifyTilesChanged(final boolean finished) { final ArrayList tilesToReturn = new ArrayList<>(mTiles); mMainExecutor.execute(() -> { if (mListener != null) { mListener.onTilesChanged(tilesToReturn); } mFinished = finished; }); } @Nullable private State getState(Collection tiles, String spec) { for (QSTile tile : tiles) { if (spec.equals(tile.getTileSpec())) { return tile.getState().copy(); } } return null; } private void addTile( String spec, @Nullable CharSequence appLabel, State state, boolean isSystem) { if (mSpecs.contains(spec)) { return; } state.dualTarget = false; // No dual targets in edit. state.expandedAccessibilityClassName = Button.class.getName(); state.secondaryLabel = (isSystem || TextUtils.equals(state.label, appLabel)) ? null : appLabel; TileInfo info = new TileInfo(spec, state, isSystem); mTiles.add(info); mSpecs.add(spec); } private void createStateAndAddTile( String spec, Drawable drawable, CharSequence label, CharSequence appLabel) { QSTile.State state = new QSTile.State(); state.state = Tile.STATE_INACTIVE; state.label = label; state.contentDescription = label; state.icon = new DrawableIcon(drawable); addTile(spec, appLabel, state, false); } public static class TileInfo { public TileInfo(String spec, QSTile.State state, boolean isSystem) { this.spec = spec; this.state = state; this.isSystem = isSystem; } public String spec; public QSTile.State state; public boolean isSystem; } public interface TileStateListener { void onTilesChanged(List tiles); } }