--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:paddingTop="20dp"
+ android:paddingStart="7dp"
+ android:paddingEnd="7dp">
+ <LinearLayout
+ android:layout_height="wrap_content"
+ android:layout_width="80dp"
+ android:orientation="vertical">
+ <ImageView
+ android:id="@+id/tile_icon"
+ android:layout_gravity="center"
+ android:layout_width="@dimen/qs_tile_icon_size"
+ android:layout_height="@dimen/qs_tile_icon_size" />
+ <TextView
+ android:id="@+id/tile_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center|bottom"
+ android:paddingTop="10dp"
+ android:gravity="center"
+ android:textSize="@dimen/qs_tile_text_size" />
+ </LinearLayout>
+</FrameLayout>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <ListView
+ android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ <TextView
+ android:paddingTop="10dp"
+ android:id="@+id/empty_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:text="@string/no_tiles_add" />
+</FrameLayout>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ** Copyright 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.
+ -->
+
+ <LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/qs_background_primary"
+ android:paddingBottom="20dp"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/notification_header_bg"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingTop="10dp"
+ android:paddingBottom="10dp"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="36dp"
+ android:layout_height="36dp" />
+ <TextView
+ android:id="@android:id/title"
+ android:paddingStart="10dp"
+ android:textColor="@android:color/white"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical" />
+ </LinearLayout>
+
+ <GridLayout
+ android:id="@+id/tile_grid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:columnCount="4" />
+
+ </LinearLayout>
<string name="qs_customize" translatable="false">Allow long-press customize in Quick Settings</string>
<string name="qs_customize_info" translatable="false">Info</string>
<string name="qs_customize_remove" translatable="false">Remove</string>
+ <string name="no_tiles_add" translatable="false">No tiles to add</string>
</resources>
mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget();
}
- protected final void refreshState() {
+ public final void refreshState() {
refreshState(null);
}
}
}
+ 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>();
mLargeCellHeight = mAllowDual ? res.getDimensionPixelSize(R.dimen.qs_dual_tile_height)
: mCellHeight;
mLargeCellWidth = mAllowDual ? (int) (mLargeCellHeight * TILE_ASPECT) : mCellWidth;
- mDualTileUnderlap = res.getDimensionPixelSize(R.dimen.qs_dual_tile_padding_vertical);
+ mDualTileUnderlap = mAllowDual
+ ? res.getDimensionPixelSize(R.dimen.qs_dual_tile_padding_vertical) : 0;
if (mColumns != columns) {
mColumns = columns;
postInvalidate();
}
@Override
- protected QSTile<?> createTile(String tileSpec) {
+ public QSTile<?> createTile(String tileSpec) {
QSTile<?> tile = super.createTile(tileSpec);
tile.setTileSpec(tileSpec);
return tile;
return mTiles;
}
+ public void addTile(String spec) {
+ mTiles.add(spec);
+ super.onTuningChanged(TILES_SETTING, null);
+ }
+
public void replace(String oldTile, String newTile) {
if (oldTile.equals(newTile)) {
return;
}
private void setHovering(boolean hovering) {
- setAlpha(hovering ? .5f : 1);
+ setAlpha(hovering ? .3f : 1);
}
@Override
import android.animation.Animator;
import android.content.ClipData;
import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnDismissListener;
import android.util.AttributeSet;
import android.util.TypedValue;
-import android.view.*;
+import android.view.ContextThemeWrapper;
+import android.view.DragEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.WindowManager;
import android.widget.LinearLayout;
+import android.widget.ListView;
import android.widget.Toolbar;
import android.widget.Toolbar.OnMenuItemClickListener;
+
import com.android.systemui.R;
import com.android.systemui.SystemUIApplication;
import com.android.systemui.qs.QSDetailClipper;
import com.android.systemui.qs.QSTile.Host.Callback;
import com.android.systemui.qs.customize.DropButton.OnDropListener;
+import com.android.systemui.qs.customize.TileAdapter.TileSelectedListener;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
import com.android.systemui.statusbar.phone.QSTileHost;
import com.android.systemui.statusbar.phone.SystemUIDialog;
* *someday* do fancy animations to get into/out of it.
*/
public class QSCustomizer extends LinearLayout implements OnMenuItemClickListener, Callback,
- OnDropListener, OnClickListener, Animator.AnimatorListener {
+ OnDropListener, OnClickListener, Animator.AnimatorListener, TileSelectedListener,
+ OnCancelListener, OnDismissListener {
private static final int MENU_SAVE = Menu.FIRST;
private static final int MENU_RESET = Menu.FIRST + 1;
private DropButton mInfoButton;
private DropButton mRemoveButton;
private FloatingActionButton mFab;
+ private SystemUIDialog mDialog;
public QSCustomizer(Context context, AttributeSet attrs) {
super(new ContextThemeWrapper(context, android.R.style.Theme_Material), attrs);
}
@Override
+ public void onTileSelected(String spec) {
+ if (mDialog != null) {
+ mHost.addTile(spec);
+ mDialog.dismiss();
+ }
+ }
+
+ @Override
public void onTilesChanged() {
mQsPanel.setTiles(mHost.getTiles());
}
@Override
public void onClick(View v) {
if (mFab == v) {
- SystemUIDialog dialog = new SystemUIDialog(mContext);
- dialog.show();
+ mDialog = new SystemUIDialog(mContext,
+ android.R.style.Theme_Material_Dialog);
+ View view = LayoutInflater.from(mContext).inflate(R.layout.qs_add_tiles_list, null);
+ ListView listView = (ListView) view.findViewById(android.R.id.list);
+ TileAdapter adapter = new TileAdapter(mContext, mHost.getTiles(), mHost);
+ adapter.setListener(this);
+ listView.setDivider(null);
+ listView.setDividerHeight(0);
+ listView.setAdapter(adapter);
+ listView.setEmptyView(view.findViewById(R.id.empty_text));
+ mDialog.setView(view);
+ mDialog.setOnDismissListener(this);
+ mDialog.setOnCancelListener(this);
+ mDialog.show();
+ // Too lazy to figure out what this will be now, but it should probably be something
+ // besides just a dialog.
+ // For now, just make it big.
+ WindowManager.LayoutParams params = mDialog.getWindow().getAttributes();
+ params.width = WindowManager.LayoutParams.MATCH_PARENT;
+ params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ mDialog.getWindow().setAttributes(params);
}
}
@Override
+ public void onDismiss(DialogInterface dialog) {
+ mDialog = null;
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ mDialog = null;
+ }
+
+ @Override
public void onAnimationEnd(Animator animation) {
if (!isShown) {
mPhoneStatusBar.getStatusBarWindow().removeView(this);
--- /dev/null
+/*
+ * 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.customize;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.GridLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+import com.android.systemui.qs.QSTile;
+import com.android.systemui.qs.QSTile.Icon;
+import com.android.systemui.qs.tiles.CustomTile;
+import com.android.systemui.statusbar.phone.QSTileHost;
+import com.android.systemui.tuner.QSPagingSwitch;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+public class TileAdapter extends BaseAdapter {
+
+ private static final String TAG = "TileAdapter";
+
+ private final ArrayList<TileGroup> mGroups = new ArrayList<>();
+ private final Context mContext;
+
+ private TileSelectedListener mListener;
+ private ArrayList<String> mCurrentTiles;
+
+ public TileAdapter(Context context, Collection<QSTile<?>> currentTiles, QSTileHost host) {
+ mContext = context;
+ addSystemTiles(currentTiles, host);
+ // TODO: Live?
+ }
+
+ private void addSystemTiles(Collection<QSTile<?>> currentTiles, QSTileHost host) {
+ try {
+ ArrayList<String> tileSpecs = new ArrayList<>();
+ for (QSTile<?> tile : currentTiles) {
+ tileSpecs.add(tile.getTileSpec());
+ }
+ mCurrentTiles = tileSpecs;
+ final TileGroup group = new TileGroup("com.android.settings", mContext);
+ // TODO: Pull this list from a more authoritative place.
+ String[] possibleTiles = QSPagingSwitch.QS_PAGE_TILES.split(",");
+ for (int i = 0; i < possibleTiles.length; i++) {
+ final String spec = possibleTiles[i];
+ if (spec.startsWith("q")) {
+ // Quick tiles can't be customized.
+ continue;
+ }
+ if (tileSpecs.contains(spec)) {
+ continue;
+ }
+ final QSTile<?> tile = host.createTile(spec);
+ // Bad, bad, very bad.
+ tile.setListening(true);
+ tile.clearState();
+ tile.refreshState();
+ tile.setListening(false);
+ new Handler(host.getLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ group.addTile(spec, tile.getState().icon, tile.getState().label, mContext);
+ }
+ });
+ }
+ // Error: Badness (10000).
+ // Serialize this work after the host's looper's queue is empty.
+ new Handler(host.getLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ new Handler(Looper.getMainLooper()).post(new Runnable() {
+ @Override
+ public void run() {
+ if (group.mTiles.size() > 0) {
+ mGroups.add(group);
+ notifyDataSetChanged();
+ }
+ new QueryTilesTask().execute();
+ }
+ });
+ }
+ });
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Couldn't load system tiles", e);
+ }
+ }
+
+ public void setListener(TileSelectedListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public int getCount() {
+ return mGroups.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mGroups.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return mGroups.get(position).getView(mContext, convertView, parent, mListener);
+ }
+
+ private static class TileGroup {
+ private final ArrayList<TileInfo> mTiles = new ArrayList<>();
+ private CharSequence mLabel;
+ private Drawable mIcon;
+
+ public TileGroup(String pkg, Context context) throws NameNotFoundException {
+ PackageManager pm = context.getPackageManager();
+ ApplicationInfo info = pm.getApplicationInfo(pkg, 0);
+ mLabel = info.loadLabel(pm);
+ mIcon = info.loadIcon(pm);
+ Log.d(TAG, "Added " + mLabel);
+ }
+
+ private void addTile(String spec, Drawable icon, String label) {
+ TileInfo info = new TileInfo();
+ info.label = label;
+ info.drawable = icon;
+ info.spec = spec;
+ mTiles.add(info);
+ }
+
+ private void addTile(String spec, Icon icon, String label, Context context) {
+ addTile(spec, icon.getDrawable(context), label);
+ }
+
+ private View getView(Context context, View convertView, ViewGroup parent,
+ final TileSelectedListener listener) {
+ if (convertView == null) {
+ convertView = LayoutInflater.from(context).inflate(R.layout.tile_listing, parent,
+ false);
+ }
+ ((TextView) convertView.findViewById(android.R.id.title)).setText(mLabel);
+ ((ImageView) convertView.findViewById(android.R.id.icon)).setImageDrawable(mIcon);
+ GridLayout grid = (GridLayout) convertView.findViewById(R.id.tile_grid);
+ final int N = mTiles.size();
+ if (grid.getChildCount() != N) {
+ grid.removeAllViews();
+ }
+ for (int i = 0; i < N; i++) {
+ if (grid.getChildCount() <= i) {
+ grid.addView(createTile(context));
+ }
+ View view = grid.getChildAt(i);
+ final TileInfo tileInfo = mTiles.get(i);
+ ((ImageView) view.findViewById(R.id.tile_icon)).setImageDrawable(tileInfo.drawable);
+ ((TextView) view.findViewById(R.id.tile_label)).setText(tileInfo.label);
+ view.setClickable(true);
+ view.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ listener.onTileSelected(tileInfo.spec);
+ }
+ });
+ }
+ return convertView;
+ }
+
+ private View createTile(Context context) {
+ return LayoutInflater.from(context).inflate(R.layout.qs_add_tile_layout, null);
+ }
+ }
+
+ private static class TileInfo {
+ private String spec;
+ private Drawable drawable;
+ private String label;
+ }
+
+ private class QueryTilesTask extends AsyncTask<Void, Void, Collection<TileGroup>> {
+ // TODO: Become non-prototype and an API.
+ private static final String TILE_ACTION = "android.intent.action.QS_TILE";
+
+ @Override
+ protected Collection<TileGroup> doInBackground(Void... params) {
+ HashMap<String, TileGroup> pkgMap = new HashMap<>();
+ PackageManager pm = mContext.getPackageManager();
+ // TODO: Handle userness.
+ List<ResolveInfo> services = pm.queryIntentServices(new Intent(TILE_ACTION), 0);
+ for (ResolveInfo info : services) {
+ String packageName = info.serviceInfo.packageName;
+ ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);
+ String spec = CustomTile.PREFIX + componentName.flattenToShortString() + ")";
+ if (mCurrentTiles.contains(spec)) {
+ continue;
+ }
+ try {
+ TileGroup group = pkgMap.get(packageName);
+ if (group == null) {
+ group = new TileGroup(packageName, mContext);
+ pkgMap.put(packageName, group);
+ }
+ Drawable icon = info.serviceInfo.loadIcon(pm);
+ CharSequence label = info.serviceInfo.loadLabel(pm);
+ group.addTile(spec, icon, label != null ? label.toString() : "null");
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, "Couldn't find resolved package... " + packageName, e);
+ }
+ }
+ return pkgMap.values();
+ }
+
+ @Override
+ protected void onPostExecute(Collection<TileGroup> result) {
+ mGroups.addAll(result);
+ notifyDataSetChanged();
+ }
+ }
+
+ public interface TileSelectedListener {
+ void onTileSelected(String spec);
+ }
+}
--- /dev/null
+/*
+ * 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.tiles;
+
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.qs.QSTile;
+
+public class CustomTile extends QSTile<QSTile.State> {
+ public static final String PREFIX = "custom(";
+
+ private final ComponentName mComponent;
+
+ private CustomTile(Host host, String action) {
+ super(host);
+ mComponent = ComponentName.unflattenFromString(action);
+ }
+
+ public static QSTile<?> create(Host host, String spec) {
+ if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) {
+ throw new IllegalArgumentException("Bad intent tile spec: " + spec);
+ }
+ final String action = spec.substring(PREFIX.length(), spec.length() - 1);
+ if (action.isEmpty()) {
+ throw new IllegalArgumentException("Empty intent tile spec action");
+ }
+ return new CustomTile(host, action);
+ }
+
+ @Override
+ public void setListening(boolean listening) {
+ }
+
+ @Override
+ protected State newTileState() {
+ return new State();
+ }
+
+ @Override
+ protected void handleUserSwitch(int newUserId) {
+ super.handleUserSwitch(newUserId);
+ }
+
+ @Override
+ protected void handleClick() {
+ MetricsLogger.action(mContext, getMetricsCategory(), mComponent.getPackageName());
+ }
+
+ @Override
+ protected void handleLongClick() {
+ }
+
+ @Override
+ protected void handleUpdateState(State state, Object arg) {
+ // TODO: Actual things.
+ try {
+ PackageManager pm = mContext.getPackageManager();
+ ServiceInfo info = pm.getServiceInfo(mComponent, 0);
+ state.visible = true;
+ state.icon = new DrawableIcon(info.loadIcon(pm));
+ state.label = info.loadLabel(pm).toString();
+ state.contentDescription = state.label;
+ } catch (Exception e) {
+ state.visible = false;
+ }
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return MetricsLogger.QS_INTENT;
+ }
+}
}
}
- protected QSTile<?> createTile(String tileSpec) {
+ public QSTile<?> createTile(String tileSpec) {
if (tileSpec.equals("wifi")) return new WifiTile(this, false);
else if (tileSpec.equals("bt")) return new BluetoothTile(this, false);
else if (tileSpec.equals("inversion")) return new ColorInversionTile(this);
else if (tileSpec.equals("qlock")) return new QLockTile(this);
// Intent tiles.
else if (tileSpec.startsWith(IntentTile.PREFIX)) return IntentTile.create(this,tileSpec);
+ else if (tileSpec.startsWith(CustomTile.PREFIX)) return CustomTile.create(this,tileSpec);
else throw new IllegalArgumentException("Bad tile spec: " + tileSpec);
}
private final Context mContext;
public SystemUIDialog(Context context) {
- super(context, R.style.Theme_SystemUI_Dialog);
+ this(context, R.style.Theme_SystemUI_Dialog);
+ }
+
+ public SystemUIDialog(Context context, int theme) {
+ super(context, theme);
mContext = context;
getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL);
}
@Override
- protected QSTile<?> createTile(String tileSpec) {
+ public QSTile<?> createTile(String tileSpec) {
return new DraggableTile(this, tileSpec);
}