blob: d3f5d2667ebffc3f9265b68d843eca7695181949 [file] [log] [blame]
/*
* Copyright (C) 2015 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.app.ActivityManager;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.provider.Settings;
import android.service.quicksettings.IQSTileService;
import android.service.quicksettings.Tile;
import android.service.quicksettings.TileService;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.view.IWindowManager;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.MetricsProto.MetricsEvent;
import com.android.systemui.R;
import com.android.systemui.qs.QSTile;
import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener;
import com.android.systemui.statusbar.phone.QSTileHost;
import libcore.util.Objects;
public class CustomTile extends QSTile<QSTile.State> implements TileChangeListener {
public static final String PREFIX = "custom(";
private static final boolean DEBUG = false;
// We don't want to thrash binding and unbinding if the user opens and closes the panel a lot.
// So instead we have a period of waiting.
private static final long UNBIND_DELAY = 30000;
private final ComponentName mComponent;
private final Tile mTile;
private final IWindowManager mWindowManager;
private final IBinder mToken = new Binder();
private final IQSTileService mService;
private final TileServiceManager mServiceManager;
private final int mUser;
private android.graphics.drawable.Icon mDefaultIcon;
private boolean mListening;
private boolean mBound;
private boolean mIsTokenGranted;
private boolean mIsShowingDialog;
private CustomTile(QSTileHost host, String action) {
super(host);
mWindowManager = WindowManagerGlobal.getWindowManagerService();
mComponent = ComponentName.unflattenFromString(action);
mTile = new Tile(mComponent);
setTileIcon();
mServiceManager = host.getTileServices().getTileWrapper(this);
mService = mServiceManager.getTileService();
mServiceManager.setTileChangeListener(this);
mUser = ActivityManager.getCurrentUser();
}
private void setTileIcon() {
try {
PackageManager pm = mContext.getPackageManager();
ServiceInfo info = pm.getServiceInfo(mComponent,
PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE);
int icon = info.icon != 0 ? info.icon
: info.applicationInfo.icon;
// Update the icon if its not set or is the default icon.
boolean updateIcon = mTile.getIcon() == null
|| iconEquals(mTile.getIcon(), mDefaultIcon);
mDefaultIcon = icon != 0 ? android.graphics.drawable.Icon
.createWithResource(mComponent.getPackageName(), icon) : null;
if (updateIcon) {
mTile.setIcon(mDefaultIcon);
}
// Update the label if there is no label.
if (mTile.getLabel() == null) {
mTile.setLabel(info.loadLabel(pm));
}
} catch (Exception e) {
mDefaultIcon = null;
}
}
/**
* Compare two icons, only works for resources.
*/
private boolean iconEquals(android.graphics.drawable.Icon icon1,
android.graphics.drawable.Icon icon2) {
if (icon1 == icon2) {
return true;
}
if (icon1 == null || icon2 == null) {
return false;
}
if (icon1.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE
|| icon2.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE) {
return false;
}
if (icon1.getResId() != icon2.getResId()) {
return false;
}
if (!Objects.equal(icon1.getResPackage(), icon2.getResPackage())) {
return false;
}
return true;
}
@Override
public void onTileChanged(ComponentName tile) {
setTileIcon();
}
@Override
public boolean isAvailable() {
return mDefaultIcon != null;
}
public int getUser() {
return mUser;
}
public ComponentName getComponent() {
return mComponent;
}
public Tile getQsTile() {
return mTile;
}
public void updateState(Tile tile) {
mTile.setIcon(tile.getIcon());
mTile.setLabel(tile.getLabel());
mTile.setContentDescription(tile.getContentDescription());
mTile.setState(tile.getState());
}
public void onDialogShown() {
mIsShowingDialog = true;
}
public void onDialogHidden() {
mIsShowingDialog = false;
try {
if (DEBUG) Log.d(TAG, "Removing token");
mWindowManager.removeWindowToken(mToken);
} catch (RemoteException e) {
}
}
@Override
public void setListening(boolean listening) {
if (mListening == listening) return;
mListening = listening;
try {
if (listening) {
setTileIcon();
refreshState();
if (!mServiceManager.isActiveTile()) {
mServiceManager.setBindRequested(true);
mService.onStartListening();
}
} else {
mService.onStopListening();
if (mIsTokenGranted && !mIsShowingDialog) {
try {
if (DEBUG) Log.d(TAG, "Removing token");
mWindowManager.removeWindowToken(mToken);
} catch (RemoteException e) {
}
mIsTokenGranted = false;
}
mIsShowingDialog = false;
mServiceManager.setBindRequested(false);
}
} catch (RemoteException e) {
// Called through wrapper, won't happen here.
}
}
@Override
protected void handleDestroy() {
super.handleDestroy();
if (mIsTokenGranted) {
try {
if (DEBUG) Log.d(TAG, "Removing token");
mWindowManager.removeWindowToken(mToken);
} catch (RemoteException e) {
}
}
mHost.getTileServices().freeService(this, mServiceManager);
}
@Override
public State newTileState() {
return new State();
}
@Override
public Intent getLongClickIntent() {
Intent i = new Intent(TileService.ACTION_QS_TILE_PREFERENCES);
i.setPackage(mComponent.getPackageName());
i = resolveIntent(i);
if (i != null) {
return i;
}
return new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
Uri.fromParts("package", mComponent.getPackageName(), null));
}
private Intent resolveIntent(Intent i) {
ResolveInfo result = mContext.getPackageManager().resolveActivityAsUser(i, 0,
ActivityManager.getCurrentUser());
return result != null ? new Intent(TileService.ACTION_QS_TILE_PREFERENCES)
.setClassName(result.activityInfo.packageName, result.activityInfo.name) : null;
}
@Override
protected void handleClick() {
if (mTile.getState() == Tile.STATE_UNAVAILABLE) {
return;
}
try {
if (DEBUG) Log.d(TAG, "Adding token");
mWindowManager.addWindowToken(mToken, WindowManager.LayoutParams.TYPE_QS_DIALOG);
mIsTokenGranted = true;
} catch (RemoteException e) {
}
try {
if (mServiceManager.isActiveTile()) {
mServiceManager.setBindRequested(true);
mService.onStartListening();
}
mService.onClick(mToken);
} catch (RemoteException e) {
// Called through wrapper, won't happen here.
}
MetricsLogger.action(mContext, getMetricsCategory(), mComponent.getPackageName());
}
@Override
public CharSequence getTileLabel() {
return getState().label;
}
@Override
protected void handleUpdateState(State state, Object arg) {
int tileState = mTile.getState();
if (mServiceManager.hasPendingBind()) {
tileState = Tile.STATE_UNAVAILABLE;
}
Drawable drawable;
try {
drawable = mTile.getIcon().loadDrawable(mContext);
} catch (Exception e) {
Log.w(TAG, "Invalid icon, forcing into unavailable state");
tileState = Tile.STATE_UNAVAILABLE;
drawable = mDefaultIcon.loadDrawable(mContext);
}
int color = mContext.getColor(getColor(tileState));
drawable.setTint(color);
state.icon = new DrawableIcon(drawable);
state.label = mTile.getLabel();
if (tileState == Tile.STATE_UNAVAILABLE) {
state.label = new SpannableStringBuilder().append(state.label,
new ForegroundColorSpan(color),
SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
}
if (mTile.getContentDescription() != null) {
state.contentDescription = mTile.getContentDescription();
} else {
state.contentDescription = state.label;
}
}
@Override
public int getMetricsCategory() {
return MetricsEvent.QS_CUSTOM;
}
public void startUnlockAndRun() {
mHost.startRunnableDismissingKeyguard(new Runnable() {
@Override
public void run() {
try {
mService.onUnlockComplete();
} catch (RemoteException e) {
}
}
});
}
private static int getColor(int state) {
switch (state) {
case Tile.STATE_UNAVAILABLE:
return R.color.qs_tile_tint_unavailable;
case Tile.STATE_INACTIVE:
return R.color.qs_tile_tint_inactive;
case Tile.STATE_ACTIVE:
return R.color.qs_tile_tint_active;
}
return 0;
}
public static String toSpec(ComponentName name) {
return PREFIX + name.flattenToShortString() + ")";
}
public static ComponentName getComponentFromSpec(String spec) {
final String action = spec.substring(PREFIX.length(), spec.length() - 1);
if (action.isEmpty()) {
throw new IllegalArgumentException("Empty custom tile spec action");
}
return ComponentName.unflattenFromString(action);
}
public static QSTile<?> create(QSTileHost host, String spec) {
if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) {
throw new IllegalArgumentException("Bad custom tile spec: " + spec);
}
final String action = spec.substring(PREFIX.length(), spec.length() - 1);
if (action.isEmpty()) {
throw new IllegalArgumentException("Empty custom tile spec action");
}
return new CustomTile(host, action);
}
}