blob: db77e08c204b3dc68814cfb5f4c5f506e14f01b7 [file] [log] [blame]
/*
* 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.app.ActivityManager;
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 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.QSTileHost;
import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon;
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;
public class TileQueryHelper {
private static final String TAG = "TileQueryHelper";
private final ArrayList<TileInfo> mTiles = new ArrayList<>();
private final ArraySet<String> mSpecs = new ArraySet<>();
private final Executor mMainExecutor;
private final Executor mBgExecutor;
private final Context mContext;
private TileStateListener mListener;
private boolean mFinished;
@Inject
public TileQueryHelper(Context context,
@Main Executor mainExecutor, @Background Executor bgExecutor) {
mContext = context;
mMainExecutor = mainExecutor;
mBgExecutor = bgExecutor;
}
public void setListener(TileStateListener listener) {
mListener = listener;
}
public void queryTiles(QSTileHost host) {
mTiles.clear();
mSpecs.clear();
mFinished = false;
// Enqueue jobs to fetch every system tile and then ever package tile.
addCurrentAndStockTiles(host);
addPackageTiles(host);
}
public boolean isFinished() {
return mFinished;
}
private void addCurrentAndStockTiles(QSTileHost host) {
String stock = mContext.getString(R.string.quick_settings_tiles_stock);
String current = Settings.Secure.getString(mContext.getContentResolver(),
Settings.Secure.QS_TILES);
final ArrayList<String> 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<QSTile> tilesToAdd = new ArrayList<>();
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.setListening(this, true);
tile.refreshState();
tile.setListening(this, false);
tile.setTileSpec(spec);
tilesToAdd.add(tile);
}
mBgExecutor.execute(() -> {
for (QSTile tile : tilesToAdd) {
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);
}
notifyTilesChanged(false);
});
}
private void addPackageTiles(final QSTileHost host) {
mBgExecutor.execute(() -> {
Collection<QSTile> params = host.getTiles();
PackageManager pm = mContext.getPackageManager();
List<ResolveInfo> services = pm.queryIntentServicesAsUser(
new Intent(TileService.ACTION_QS_TILE), 0, ActivityManager.getCurrentUser());
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<TileInfo> tilesToReturn = new ArrayList<>(mTiles);
mMainExecutor.execute(() -> {
if (mListener != null) {
mListener.onTilesChanged(tilesToReturn);
}
mFinished = finished;
});
}
private State getState(Collection<QSTile> tiles, String spec) {
for (QSTile tile : tiles) {
if (spec.equals(tile.getTileSpec())) {
return tile.getState().copy();
}
}
return null;
}
private void addTile(String spec, CharSequence appLabel, State state, boolean isSystem) {
if (mSpecs.contains(spec)) {
return;
}
TileInfo info = new TileInfo();
info.state = state;
info.state.dualTarget = false; // No dual targets in edit.
info.state.expandedAccessibilityClassName =
Button.class.getName();
info.spec = spec;
info.state.secondaryLabel = (isSystem || TextUtils.equals(state.label, appLabel))
? null : appLabel;
info.isSystem = 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 String spec;
public QSTile.State state;
public boolean isSystem;
}
public interface TileStateListener {
void onTilesChanged(List<TileInfo> tiles);
}
}