/* * Copyright (C) 2017 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.tileimpl; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_CLICK; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_LONG_PRESS; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_SECONDARY_CLICK; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_CONTEXT; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_POSITION; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.FIELD_QS_VALUE; import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.TYPE_ACTION; import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Drawable; import android.metrics.LogMaker; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.service.quicksettings.Tile; import android.text.format.DateUtils; import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.Utils; import com.android.systemui.Dependency; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.plugins.qs.QSIconView; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.qs.PagedTileLayout.TilePage; import com.android.systemui.qs.QSHost; import java.util.ArrayList; /** * Base quick-settings tile, extend this to create a new tile. * * State management done on a looper provided by the host. Tiles should update state in * handleUpdateState. Callbacks affecting state should use refreshState to trigger another * state update pass on tile looper. */ public abstract class QSTileImpl implements QSTile { protected final String TAG = "Tile." + getClass().getSimpleName(); protected static final boolean DEBUG = Log.isLoggable("Tile", Log.DEBUG); private static final long DEFAULT_STALE_TIMEOUT = 10 * DateUtils.MINUTE_IN_MILLIS; protected final QSHost mHost; protected final Context mContext; // @NonFinalForTesting protected H mHandler = new H(Dependency.get(Dependency.BG_LOOPER)); protected final Handler mUiHandler = new Handler(Looper.getMainLooper()); private final ArraySet mListeners = new ArraySet<>(); private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); private final ArrayList mCallbacks = new ArrayList<>(); private final Object mStaleListener = new Object(); protected TState mState = newTileState(); private TState mTmpState = newTileState(); private boolean mAnnounceNextStateChange; private String mTileSpec; private EnforcedAdmin mEnforcedAdmin; private boolean mShowingDetail; public abstract TState newTileState(); abstract protected void handleClick(); abstract protected void handleUpdateState(TState state, Object arg); /** * Declare the category of this tile. * * Categories are defined in {@link com.android.internal.logging.nano.MetricsProto.MetricsEvent} * by editing frameworks/base/proto/src/metrics_constants.proto. */ abstract public int getMetricsCategory(); protected QSTileImpl(QSHost host) { mHost = host; mContext = host.getContext(); handleStale(); // Tile was just created, must be stale. } /** * Adds or removes a listening client for the tile. If the tile has one or more * listening client it will go into the listening state. */ public void setListening(Object listener, boolean listening) { if (listening) { if (mListeners.add(listener) && mListeners.size() == 1) { if (DEBUG) Log.d(TAG, "setListening " + true); mHandler.obtainMessage(H.SET_LISTENING, 1, 0).sendToTarget(); refreshState(); // Ensure we get at least one refresh after listening. } } else { if (mListeners.remove(listener) && mListeners.size() == 0) { if (DEBUG) Log.d(TAG, "setListening " + false); mHandler.obtainMessage(H.SET_LISTENING, 0, 0).sendToTarget(); } } } protected long getStaleTimeout() { return DEFAULT_STALE_TIMEOUT; } @VisibleForTesting protected void handleStale() { setListening(mStaleListener, true); } public String getTileSpec() { return mTileSpec; } public void setTileSpec(String tileSpec) { mTileSpec = tileSpec; } public QSHost getHost() { return mHost; } public QSIconView createTileView(Context context) { return new QSIconViewImpl(context); } public DetailAdapter getDetailAdapter() { return null; // optional } protected DetailAdapter createDetailAdapter() { throw new UnsupportedOperationException(); } /** * Is a startup check whether this device currently supports this tile. * Should not be used to conditionally hide tiles. Only checked on tile * creation or whether should be shown in edit screen. */ public boolean isAvailable() { return true; } // safe to call from any thread public void addCallback(Callback callback) { mHandler.obtainMessage(H.ADD_CALLBACK, callback).sendToTarget(); } public void removeCallback(Callback callback) { mHandler.obtainMessage(H.REMOVE_CALLBACK, callback).sendToTarget(); } public void removeCallbacks() { mHandler.sendEmptyMessage(H.REMOVE_CALLBACKS); } public void click() { mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION))); mHandler.sendEmptyMessage(H.CLICK); } public void secondaryClick() { mMetricsLogger.write(populate(new LogMaker(ACTION_QS_SECONDARY_CLICK).setType(TYPE_ACTION))); mHandler.sendEmptyMessage(H.SECONDARY_CLICK); } public void longClick() { mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION))); mHandler.sendEmptyMessage(H.LONG_CLICK); } public LogMaker populate(LogMaker logMaker) { if (mState instanceof BooleanState) { logMaker.addTaggedData(FIELD_QS_VALUE, ((BooleanState) mState).value ? 1 : 0); } return logMaker.setSubtype(getMetricsCategory()) .addTaggedData(FIELD_CONTEXT, isFullQs()) .addTaggedData(FIELD_QS_POSITION, mHost.indexOf(mTileSpec)); } private int isFullQs() { for (Object listener : mListeners) { if (TilePage.class.equals(listener.getClass())) { return 1; } } return 0; } public void showDetail(boolean show) { mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget(); } public void refreshState() { refreshState(null); } protected final void refreshState(Object arg) { mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget(); } public void clearState() { mHandler.sendEmptyMessage(H.CLEAR_STATE); } public void userSwitch(int newUserId) { mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget(); } public void fireToggleStateChanged(boolean state) { mHandler.obtainMessage(H.TOGGLE_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget(); } public void fireScanStateChanged(boolean state) { mHandler.obtainMessage(H.SCAN_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget(); } public void destroy() { mHandler.sendEmptyMessage(H.DESTROY); } public TState getState() { return mState; } public void setDetailListening(boolean listening) { // optional } // call only on tile worker looper private void handleAddCallback(Callback callback) { mCallbacks.add(callback); callback.onStateChanged(mState); } private void handleRemoveCallback(Callback callback) { mCallbacks.remove(callback); } private void handleRemoveCallbacks() { mCallbacks.clear(); } protected void handleSecondaryClick() { // Default to normal click. handleClick(); } protected void handleLongClick() { Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard( getLongClickIntent(), 0); } public abstract Intent getLongClickIntent(); protected void handleClearState() { mTmpState = newTileState(); mState = newTileState(); } protected void handleRefreshState(Object arg) { handleUpdateState(mTmpState, arg); final boolean changed = mTmpState.copyTo(mState); if (changed) { handleStateChanged(); } mHandler.removeMessages(H.STALE); mHandler.sendEmptyMessageDelayed(H.STALE, getStaleTimeout()); setListening(mStaleListener, false); } private void handleStateChanged() { boolean delayAnnouncement = shouldAnnouncementBeDelayed(); if (mCallbacks.size() != 0) { for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).onStateChanged(mState); } if (mAnnounceNextStateChange && !delayAnnouncement) { String announcement = composeChangeAnnouncement(); if (announcement != null) { mCallbacks.get(0).onAnnouncementRequested(announcement); } } } mAnnounceNextStateChange = mAnnounceNextStateChange && delayAnnouncement; } protected boolean shouldAnnouncementBeDelayed() { return false; } protected String composeChangeAnnouncement() { return null; } private void handleShowDetail(boolean show) { mShowingDetail = show; for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).onShowDetail(show); } } protected boolean isShowingDetail() { return mShowingDetail; } private void handleToggleStateChanged(boolean state) { for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).onToggleStateChanged(state); } } private void handleScanStateChanged(boolean state) { for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).onScanStateChanged(state); } } protected void handleUserSwitch(int newUserId) { handleRefreshState(null); } protected abstract void handleSetListening(boolean listening); protected void handleDestroy() { if (mListeners.size() != 0) { handleSetListening(false); } mCallbacks.clear(); } protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) { EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(mContext, userRestriction, ActivityManager.getCurrentUser()); if (admin != null && !RestrictedLockUtils.hasBaseUserRestriction(mContext, userRestriction, ActivityManager.getCurrentUser())) { state.disabledByPolicy = true; mEnforcedAdmin = admin; } else { state.disabledByPolicy = false; mEnforcedAdmin = null; } } public abstract CharSequence getTileLabel(); public static int getColorForState(Context context, int state) { switch (state) { case Tile.STATE_UNAVAILABLE: return Utils.getDisabled(context, Utils.getColorAttr(context, android.R.attr.colorForeground)); case Tile.STATE_INACTIVE: return Utils.getColorAttr(context, android.R.attr.textColorHint); case Tile.STATE_ACTIVE: return Utils.getColorAttr(context, android.R.attr.textColorPrimary); default: Log.e("QSTile", "Invalid state " + state); return 0; } } protected final class H extends Handler { private static final int ADD_CALLBACK = 1; private static final int CLICK = 2; private static final int SECONDARY_CLICK = 3; private static final int LONG_CLICK = 4; private static final int REFRESH_STATE = 5; private static final int SHOW_DETAIL = 6; private static final int USER_SWITCH = 7; private static final int TOGGLE_STATE_CHANGED = 8; private static final int SCAN_STATE_CHANGED = 9; private static final int DESTROY = 10; private static final int CLEAR_STATE = 11; private static final int REMOVE_CALLBACKS = 12; private static final int REMOVE_CALLBACK = 13; private static final int SET_LISTENING = 14; private static final int STALE = 15; @VisibleForTesting protected H(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { String name = null; try { if (msg.what == ADD_CALLBACK) { name = "handleAddCallback"; handleAddCallback((QSTile.Callback) msg.obj); } else if (msg.what == REMOVE_CALLBACKS) { name = "handleRemoveCallbacks"; handleRemoveCallbacks(); } else if (msg.what == REMOVE_CALLBACK) { name = "handleRemoveCallback"; handleRemoveCallback((QSTile.Callback) msg.obj); } else if (msg.what == CLICK) { name = "handleClick"; if (mState.disabledByPolicy) { Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent( mContext, mEnforcedAdmin); Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard( intent, 0); } else { handleClick(); } } else if (msg.what == SECONDARY_CLICK) { name = "handleSecondaryClick"; handleSecondaryClick(); } else if (msg.what == LONG_CLICK) { name = "handleLongClick"; handleLongClick(); } else if (msg.what == REFRESH_STATE) { name = "handleRefreshState"; handleRefreshState(msg.obj); } else if (msg.what == SHOW_DETAIL) { name = "handleShowDetail"; handleShowDetail(msg.arg1 != 0); } else if (msg.what == USER_SWITCH) { name = "handleUserSwitch"; handleUserSwitch(msg.arg1); } else if (msg.what == TOGGLE_STATE_CHANGED) { name = "handleToggleStateChanged"; handleToggleStateChanged(msg.arg1 != 0); } else if (msg.what == SCAN_STATE_CHANGED) { name = "handleScanStateChanged"; handleScanStateChanged(msg.arg1 != 0); } else if (msg.what == DESTROY) { name = "handleDestroy"; handleDestroy(); } else if (msg.what == CLEAR_STATE) { name = "handleClearState"; handleClearState(); } else if (msg.what == SET_LISTENING) { name = "handleSetListening"; handleSetListening(msg.arg1 != 0); } else if (msg.what == STALE) { name = "handleStale"; handleStale(); } else { throw new IllegalArgumentException("Unknown msg: " + msg.what); } } catch (Throwable t) { final String error = "Error in " + name; Log.w(TAG, error, t); mHost.warn(error, t); } } } public static class DrawableIcon extends Icon { protected final Drawable mDrawable; public DrawableIcon(Drawable drawable) { mDrawable = drawable; } @Override public Drawable getDrawable(Context context) { return mDrawable; } } public static class DrawableIconWithRes extends DrawableIcon { private final int mId; public DrawableIconWithRes(Drawable drawable, int id) { super(drawable); mId = id; } @Override public boolean equals(Object o) { return o instanceof DrawableIconWithRes && ((DrawableIconWithRes) o).mId == mId; } } public static class ResourceIcon extends Icon { private static final SparseArray ICONS = new SparseArray(); protected final int mResId; private ResourceIcon(int resId) { mResId = resId; } public static Icon get(int resId) { Icon icon = ICONS.get(resId); if (icon == null) { icon = new ResourceIcon(resId); ICONS.put(resId, icon); } return icon; } @Override public Drawable getDrawable(Context context) { return context.getDrawable(mResId); } @Override public Drawable getInvisibleDrawable(Context context) { return context.getDrawable(mResId); } @Override public boolean equals(Object o) { return o instanceof ResourceIcon && ((ResourceIcon) o).mResId == mResId; } @Override public String toString() { return String.format("ResourceIcon[resId=0x%08x]", mResId); } } protected static class AnimationIcon extends ResourceIcon { private final int mAnimatedResId; public AnimationIcon(int resId, int staticResId) { super(staticResId); mAnimatedResId = resId; } @Override public Drawable getDrawable(Context context) { // workaround: get a clean state for every new AVD return context.getDrawable(mAnimatedResId).getConstantState().newDrawable(); } } }