| /* |
| * Copyright (C) 2014 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; |
| |
| import android.app.PendingIntent; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.graphics.drawable.Drawable; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import com.android.systemui.qs.QSTile.State; |
| import com.android.systemui.qs.external.TileServices; |
| import com.android.systemui.statusbar.policy.BatteryController; |
| import com.android.systemui.statusbar.policy.BluetoothController; |
| import com.android.systemui.statusbar.policy.CastController; |
| import com.android.systemui.statusbar.policy.FlashlightController; |
| import com.android.systemui.statusbar.policy.HotspotController; |
| import com.android.systemui.statusbar.policy.KeyguardMonitor; |
| import com.android.systemui.statusbar.policy.Listenable; |
| import com.android.systemui.statusbar.policy.LocationController; |
| import com.android.systemui.statusbar.policy.NetworkController; |
| import com.android.systemui.statusbar.policy.RotationLockController; |
| import com.android.systemui.statusbar.policy.UserInfoController; |
| import com.android.systemui.statusbar.policy.UserSwitcherController; |
| import com.android.systemui.statusbar.policy.ZenModeController; |
| |
| import java.util.Collection; |
| import java.util.Objects; |
| |
| /** |
| * 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 QSTile<TState extends State> implements Listenable { |
| protected final String TAG = "Tile." + getClass().getSimpleName(); |
| protected static final boolean DEBUG = Log.isLoggable("Tile", Log.DEBUG); |
| |
| protected final Host mHost; |
| protected final Context mContext; |
| protected final H mHandler; |
| protected final Handler mUiHandler = new Handler(Looper.getMainLooper()); |
| |
| private Callback mCallback; |
| protected TState mState = newTileState(); |
| private TState mTmpState = newTileState(); |
| private boolean mAnnounceNextStateChange; |
| |
| private String mTileSpec; |
| |
| abstract protected 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.MetricsLogger} |
| * or if there is no relevant existing category you may define one in |
| * {@link com.android.systemui.qs.QSTile}. |
| */ |
| abstract public int getMetricsCategory(); |
| |
| protected QSTile(Host host) { |
| mHost = host; |
| mContext = host.getContext(); |
| mHandler = new H(host.getLooper()); |
| } |
| |
| public String getTileSpec() { |
| return mTileSpec; |
| } |
| |
| public void setTileSpec(String tileSpec) { |
| mTileSpec = tileSpec; |
| } |
| |
| public Host getHost() { |
| return mHost; |
| } |
| |
| public QSIconView createTileView(Context context) { |
| return new QSIconView(context); |
| } |
| |
| public DetailAdapter getDetailAdapter() { |
| return null; // optional |
| } |
| |
| public interface DetailAdapter { |
| int getTitle(); |
| Boolean getToggleState(); |
| View createDetailView(Context context, View convertView, ViewGroup parent); |
| Intent getSettingsIntent(); |
| void setToggleState(boolean state); |
| int getMetricsCategory(); |
| } |
| |
| // safe to call from any thread |
| |
| public void setCallback(Callback callback) { |
| mHandler.obtainMessage(H.SET_CALLBACK, callback).sendToTarget(); |
| } |
| |
| public void click() { |
| mHandler.sendEmptyMessage(H.CLICK); |
| } |
| |
| public void secondaryClick() { |
| mHandler.sendEmptyMessage(H.SECONDARY_CLICK); |
| } |
| |
| public void longClick() { |
| mHandler.sendEmptyMessage(H.LONG_CLICK); |
| } |
| |
| public void showDetail(boolean show) { |
| mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget(); |
| } |
| |
| public final void refreshState() { |
| refreshState(null); |
| } |
| |
| protected final void refreshState(Object arg) { |
| mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget(); |
| } |
| |
| public final 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 handleSetCallback(Callback callback) { |
| mCallback = callback; |
| handleRefreshState(null); |
| } |
| |
| protected void handleSecondaryClick() { |
| // Default to normal click. |
| handleClick(); |
| } |
| |
| protected void handleLongClick() { |
| // optional |
| } |
| |
| protected void handleClearState() { |
| mTmpState = newTileState(); |
| mState = newTileState(); |
| } |
| |
| protected void handleRefreshState(Object arg) { |
| handleUpdateState(mTmpState, arg); |
| final boolean changed = mTmpState.copyTo(mState); |
| if (changed) { |
| handleStateChanged(); |
| } |
| } |
| |
| private void handleStateChanged() { |
| boolean delayAnnouncement = shouldAnnouncementBeDelayed(); |
| if (mCallback != null) { |
| mCallback.onStateChanged(mState); |
| if (mAnnounceNextStateChange && !delayAnnouncement) { |
| String announcement = composeChangeAnnouncement(); |
| if (announcement != null) { |
| mCallback.onAnnouncementRequested(announcement); |
| } |
| } |
| } |
| mAnnounceNextStateChange = mAnnounceNextStateChange && delayAnnouncement; |
| } |
| |
| protected boolean shouldAnnouncementBeDelayed() { |
| return false; |
| } |
| |
| protected String composeChangeAnnouncement() { |
| return null; |
| } |
| |
| private void handleShowDetail(boolean show) { |
| if (mCallback != null) { |
| mCallback.onShowDetail(show); |
| } |
| } |
| |
| private void handleToggleStateChanged(boolean state) { |
| if (mCallback != null) { |
| mCallback.onToggleStateChanged(state); |
| } |
| } |
| |
| private void handleScanStateChanged(boolean state) { |
| if (mCallback != null) { |
| mCallback.onScanStateChanged(state); |
| } |
| } |
| |
| protected void handleUserSwitch(int newUserId) { |
| handleRefreshState(null); |
| } |
| |
| protected void handleDestroy() { |
| setListening(false); |
| mCallback = null; |
| } |
| |
| protected final class H extends Handler { |
| private static final int SET_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 H(Looper looper) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| String name = null; |
| try { |
| if (msg.what == SET_CALLBACK) { |
| name = "handleSetCallback"; |
| handleSetCallback((QSTile.Callback)msg.obj); |
| } else if (msg.what == CLICK) { |
| name = "handleClick"; |
| mAnnounceNextStateChange = true; |
| 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 { |
| 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 interface Callback { |
| void onStateChanged(State state); |
| void onShowDetail(boolean show); |
| void onToggleStateChanged(boolean state); |
| void onScanStateChanged(boolean state); |
| void onAnnouncementRequested(CharSequence announcement); |
| } |
| |
| public interface Host { |
| void startActivityDismissingKeyguard(Intent intent); |
| void startActivityDismissingKeyguard(PendingIntent intent); |
| void startRunnableDismissingKeyguard(Runnable runnable); |
| void warn(String message, Throwable t); |
| void collapsePanels(); |
| void openPanels(); |
| Looper getLooper(); |
| Context getContext(); |
| Collection<QSTile<?>> getTiles(); |
| void addCallback(Callback callback); |
| BluetoothController getBluetoothController(); |
| LocationController getLocationController(); |
| RotationLockController getRotationLockController(); |
| NetworkController getNetworkController(); |
| ZenModeController getZenModeController(); |
| HotspotController getHotspotController(); |
| CastController getCastController(); |
| FlashlightController getFlashlightController(); |
| KeyguardMonitor getKeyguardMonitor(); |
| UserSwitcherController getUserSwitcherController(); |
| UserInfoController getUserInfoController(); |
| BatteryController getBatteryController(); |
| TileServices getTileServices(); |
| void removeTile(String tileSpec); |
| |
| |
| public interface Callback { |
| void onTilesChanged(); |
| } |
| } |
| |
| public static abstract class Icon { |
| abstract public Drawable getDrawable(Context context); |
| |
| @Override |
| public int hashCode() { |
| return Icon.class.hashCode(); |
| } |
| } |
| |
| 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 ResourceIcon extends Icon { |
| private static final SparseArray<Icon> ICONS = new SparseArray<Icon>(); |
| |
| 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 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 class AnimationIcon extends ResourceIcon { |
| public AnimationIcon(int resId) { |
| super(resId); |
| } |
| |
| @Override |
| public Drawable getDrawable(Context context) { |
| // workaround: get a clean state for every new AVD |
| return context.getDrawable(mResId).getConstantState().newDrawable(); |
| } |
| } |
| |
| public static class State { |
| public Icon icon; |
| public CharSequence label; |
| public CharSequence contentDescription; |
| public CharSequence dualLabelContentDescription; |
| public boolean autoMirrorDrawable = true; |
| |
| public boolean copyTo(State other) { |
| if (other == null) throw new IllegalArgumentException(); |
| if (!other.getClass().equals(getClass())) throw new IllegalArgumentException(); |
| final boolean changed = !Objects.equals(other.icon, icon) |
| || !Objects.equals(other.label, label) |
| || !Objects.equals(other.contentDescription, contentDescription) |
| || !Objects.equals(other.autoMirrorDrawable, autoMirrorDrawable) |
| || !Objects.equals(other.dualLabelContentDescription, |
| dualLabelContentDescription); |
| other.icon = icon; |
| other.label = label; |
| other.contentDescription = contentDescription; |
| other.dualLabelContentDescription = dualLabelContentDescription; |
| other.autoMirrorDrawable = autoMirrorDrawable; |
| return changed; |
| } |
| |
| @Override |
| public String toString() { |
| return toStringBuilder().toString(); |
| } |
| |
| protected StringBuilder toStringBuilder() { |
| final StringBuilder sb = new StringBuilder(getClass().getSimpleName()).append('['); |
| sb.append(",icon=").append(icon); |
| sb.append(",label=").append(label); |
| sb.append(",contentDescription=").append(contentDescription); |
| sb.append(",dualLabelContentDescription=").append(dualLabelContentDescription); |
| sb.append(",autoMirrorDrawable=").append(autoMirrorDrawable); |
| return sb.append(']'); |
| } |
| } |
| |
| public static class BooleanState extends State { |
| public boolean value; |
| |
| @Override |
| public boolean copyTo(State other) { |
| final BooleanState o = (BooleanState) other; |
| final boolean changed = super.copyTo(other) || o.value != value; |
| o.value = value; |
| return changed; |
| } |
| |
| @Override |
| protected StringBuilder toStringBuilder() { |
| final StringBuilder rt = super.toStringBuilder(); |
| rt.insert(rt.length() - 1, ",value=" + value); |
| return rt; |
| } |
| } |
| |
| public static final class SignalState extends State { |
| public boolean enabled; |
| public boolean connected; |
| public boolean activityIn; |
| public boolean activityOut; |
| public int overlayIconId; |
| public boolean filter; |
| public boolean isOverlayIconWide; |
| |
| @Override |
| public boolean copyTo(State other) { |
| final SignalState o = (SignalState) other; |
| final boolean changed = o.enabled != enabled |
| || o.connected != connected || o.activityIn != activityIn |
| || o.activityOut != activityOut |
| || o.overlayIconId != overlayIconId |
| || o.isOverlayIconWide != isOverlayIconWide; |
| o.enabled = enabled; |
| o.connected = connected; |
| o.activityIn = activityIn; |
| o.activityOut = activityOut; |
| o.overlayIconId = overlayIconId; |
| o.filter = filter; |
| o.isOverlayIconWide = isOverlayIconWide; |
| return super.copyTo(other) || changed; |
| } |
| |
| @Override |
| protected StringBuilder toStringBuilder() { |
| final StringBuilder rt = super.toStringBuilder(); |
| rt.insert(rt.length() - 1, ",enabled=" + enabled); |
| rt.insert(rt.length() - 1, ",connected=" + connected); |
| rt.insert(rt.length() - 1, ",activityIn=" + activityIn); |
| rt.insert(rt.length() - 1, ",activityOut=" + activityOut); |
| rt.insert(rt.length() - 1, ",overlayIconId=" + overlayIconId); |
| rt.insert(rt.length() - 1, ",filter=" + filter); |
| rt.insert(rt.length() - 1, ",wideOverlayIcon=" + isOverlayIconWide); |
| return rt; |
| } |
| } |
| } |