Merge "Rewrite QS customizer"

This commit is contained in:
Jason Monk
2016-02-04 16:22:24 +00:00
committed by Android (Google) Code Review
32 changed files with 535 additions and 1112 deletions

View File

@@ -1,43 +0,0 @@
<?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>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 The Android Open Source Project
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.
@@ -14,18 +14,16 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<com.android.systemui.qs.customize.NonPagedTileLayout
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tiles_container"
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<view
class="com.android.systemui.qs.PagedTileLayout$TilePage"
android:id="@+id/tile_page"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.android.systemui.qs.customize.NonPagedTileLayout>
android:paddingTop="20dp"
android:paddingStart="16dp"
android:paddingEnd="8dp"
android:paddingBottom="13dp"
android:textAppearance="@android:style/TextAppearance.Material.Body2"
android:textColor="?android:attr/colorAccent"
android:text="@string/drag_to_add_tiles" />

View File

@@ -22,85 +22,53 @@
android:background="@drawable/qs_customizer_background"
android:gravity="center_horizontal">
<FrameLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/notification_header_bg">
android:paddingTop="28dp"
android:paddingEnd="8dp">
<LinearLayout
android:id="@+id/drag_buttons"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:orientation="horizontal">
<FrameLayout
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="1">
<com.android.systemui.qs.customize.DropButton
android:id="@+id/info_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:drawableStart="@drawable/ic_info"
android:drawablePadding="10dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="@android:color/white"
android:text="@string/qs_customize_info" />
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="1">
<com.android.systemui.qs.customize.DropButton
android:id="@+id/remove_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:drawableStart="@drawable/ic_close_white"
android:drawablePadding="10dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="@android:color/white"
android:text="@string/qs_customize_remove" />
</FrameLayout>
</LinearLayout>
<ImageView
android:id="@+id/close"
android:layout_width="56dp"
android:layout_height="56dp"
android:padding="16dp"
android:src="@drawable/ic_close_white" />
<Toolbar
android:id="@*android:id/action_bar"
android:layout_width="match_parent"
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:navigationContentDescription="@*android:string/action_bar_up_description"
android:background="@drawable/notification_header_bg"
style="?android:attr/toolbarStyle" />
</FrameLayout>
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:background="?android:attr/selectableItemBackground"
android:textAppearance="@android:style/TextAppearance.Material.Widget.Button.Inverse"
android:textColor="?android:attr/colorAccent"
android:text="@string/save" />
<com.android.systemui.tuner.AutoScrollView
<Button
android:id="@+id/reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:background="?android:attr/selectableItemBackground"
android:textAppearance="@android:style/TextAppearance.Material.Widget.Button.Inverse"
android:textColor="?android:attr/colorAccent"
android:text="@*android:string/reset" />
</LinearLayout>
<android.support.v7.widget.RecyclerView
android:id="@android:id/list"
android:layout_width="@dimen/notification_panel_width"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingTop="12dp"
android:paddingBottom="8dp"
android:elevation="2dp">
<com.android.systemui.qs.customize.CustomQSPanel
android:id="@+id/quick_settings_panel"
android:background="#0000"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.android.systemui.tuner.AutoScrollView>
<com.android.systemui.qs.customize.FloatingActionButton
android:id="@+id/fab"
android:clickable="true"
android:layout_width="@dimen/fab_size"
android:layout_height="@dimen/fab_size"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/fab_margin"
android:layout_marginBottom="@dimen/fab_margin"
android:elevation="@dimen/fab_elevation"
android:background="@drawable/fab_background" />
android:layout_weight="1" />
<View
android:layout_width="match_parent"

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingBottom="16dp" />

View File

@@ -1400,4 +1400,7 @@
<!-- SysUI Tuner: Label for preview area in navigation bar tuner [CHAR LIMIT=NONE] -->
<string name="preview">Preview</string>
<!-- Label for area where tiles can be dragged out of [CHAR LIMIT=60] -->
<string name="drag_to_add_tiles">Drag to add tiles</string>
</resources>

View File

@@ -76,7 +76,7 @@ public abstract class QSTile<TState extends State> implements Listenable {
private String mTileSpec;
abstract protected TState newTileState();
public abstract TState newTileState();
abstract protected void handleClick();
abstract protected void handleUpdateState(TState state, Object arg);

View File

@@ -1,91 +0,0 @@
/*
* 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.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.graphics.drawable.Drawable;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.MetricsProto.MetricsEvent;
import com.android.systemui.R;
import com.android.systemui.qs.QSTile;
public class BlankCustomTile extends QSTile<QSTile.State> {
public static final String PREFIX = "custom(";
private final ComponentName mComponent;
private BlankCustomTile(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 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 BlankCustomTile(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) {
try {
PackageManager pm = mContext.getPackageManager();
ServiceInfo info = pm.getServiceInfo(mComponent, 0);
Drawable drawable = info.loadIcon(pm);
drawable.setTint(mContext.getColor(R.color.qs_tile_tint_active));
state.icon = new DrawableIcon(drawable);
state.label = info.loadLabel(pm).toString();
state.contentDescription = state.label;
} catch (Exception e) {
}
}
@Override
public int getMetricsCategory() {
return MetricsEvent.QS_INTENT;
}
}

View File

@@ -1,204 +0,0 @@
/*
* 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.app.ActivityManager;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.UserHandle;
import android.provider.Settings.Secure;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import com.android.systemui.R;
import com.android.systemui.qs.QSPanel;
import com.android.systemui.qs.QSTile;
import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.qs.external.TileLifecycleManager;
import com.android.systemui.statusbar.phone.QSTileHost;
import com.android.systemui.tuner.TunerService;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* A version of QSPanel that allows tiles to be dragged around rather than
* clicked on. Dragging starting and receiving is handled in the NonPagedTileLayout,
* and the saving/ordering is handled by the CustomQSTileHost.
*/
public class CustomQSPanel extends QSPanel {
private static final String TAG = "CustomQSPanel";
private static final boolean DEBUG = false;
private List<String> mSavedTiles = Collections.emptyList();
private ArrayList<String> mStash;
private List<String> mTiles = new ArrayList<>();
private ArrayList<QSTile<?>> mCurrentTiles = new ArrayList<>();
public CustomQSPanel(Context context, AttributeSet attrs) {
super(context, attrs);
mTileLayout = (QSTileLayout) LayoutInflater.from(mContext)
.inflate(R.layout.qs_customize_layout, mQsContainer, false);
mQsContainer.addView((View) mTileLayout, 1 /* Between brightness and footer */);
((NonPagedTileLayout) mTileLayout).setCustomQsPanel(this);
removeView(mFooter.getView());
if (DEBUG) Log.d(TAG, "new CustomQSPanel", new Throwable());
TunerService.get(mContext).addTunable(this, QSTileHost.TILES_SETTING);
}
@Override
protected void showDetail(boolean show, Record r) {
// No detail here.
}
@Override
protected void onDetachedFromWindow() {
// Don't allow the super to unregister the tunable.
}
@Override
public void onTuningChanged(String key, String newValue) {
if (key.equals(QS_SHOW_BRIGHTNESS)) {
// No Brightness for you.
super.onTuningChanged(key, "0");
}
if (QSTileHost.TILES_SETTING.equals(key)) {
mSavedTiles = Collections.unmodifiableList(
QSTileHost.loadTileSpecs(mContext, newValue));
if (DEBUG) Log.d(TAG, "New saved tiles " + TextUtils.join(",", mSavedTiles));
}
}
@Override
protected void createCustomizePanel() {
// Already in CustomizePanel.
}
public void tileSelected(QSTile<?> tile, ClipData currentClip) {
String sourceSpec = getSpec(currentClip);
String destSpec = tile.getTileSpec();
if (!sourceSpec.equals(destSpec)) {
moveTo(sourceSpec, destSpec);
}
}
public ClipData getClip(QSTile<?> tile) {
String tileSpec = tile.getTileSpec();
// TODO: Something better than plain text.
// TODO: Once using something better than plain text, stop listening to non-QS drag events.
return ClipData.newPlainText(tileSpec, tileSpec);
}
public String getSpec(ClipData data) {
return data.getItemAt(0).getText().toString();
}
public void setSavedTiles() {
if (DEBUG) Log.d(TAG, "setSavedTiles " + TextUtils.join(",", mSavedTiles));
setTiles(mSavedTiles);
}
public void saveCurrentTiles() {
mHost.changeTiles(mSavedTiles, mTiles);
}
public void stashCurrentTiles() {
mStash = new ArrayList<>(mTiles);
}
public void unstashTiles() {
setTiles(mStash);
}
@Override
public void setTiles(Collection<QSTile<?>> tiles) {
setTilesInternal();
}
private void setTilesInternal() {
if (DEBUG) Log.d(TAG, "Set tiles internal");
for (int i = 0; i < mCurrentTiles.size(); i++) {
mCurrentTiles.get(i).destroy();
}
mCurrentTiles.clear();
for (int i = 0; i < mTiles.size(); i++) {
if (mTiles.get(i).startsWith(CustomTile.PREFIX)) {
QSTile<?> tile = BlankCustomTile.create(mHost, mTiles.get(i));
tile.setTileSpec(mTiles.get(i));
mCurrentTiles.add(tile);
} else {
QSTile<?> tile = mHost.createTile(mTiles.get(i));
if (tile != null) {
tile.setTileSpec(mTiles.get(i));
mCurrentTiles.add(tile);
} else {
if (DEBUG) Log.d(TAG, "Skipping " + mTiles.get(i));
}
}
}
super.setTiles(mCurrentTiles);
}
public void addTile(String spec) {
if (DEBUG) Log.d(TAG, "addTile " + spec);
mTiles.add(spec);
setTilesInternal();
}
public void moveTo(String from, String to) {
if (DEBUG) Log.d(TAG, "moveTo " + from + " " + to);
int fromIndex = mTiles.indexOf(from);
if (fromIndex < 0) {
Log.e(TAG, "Unknown from tile " + from);
return;
}
int index = mTiles.indexOf(to);
if (index < 0) {
Log.e(TAG, "Unknown to tile " + to);
return;
}
mTiles.remove(fromIndex);
mTiles.add(index, from);
setTilesInternal();
}
public void remove(String spec) {
if (!mTiles.remove(spec)) {
Log.e(TAG, "Unknown remove spec " + spec);
}
setTilesInternal();
}
public void setTiles(List<String> tiles) {
if (DEBUG) Log.d(TAG, "Set tiles " + TextUtils.join(",", tiles));
mTiles = new ArrayList<>(tiles);
setTilesInternal();
}
public Collection<QSTile<?>> getTiles() {
return mCurrentTiles;
}
}

View File

@@ -1,70 +0,0 @@
/*
* 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.ClipData;
import android.content.Context;
import android.util.AttributeSet;
import android.view.DragEvent;
import android.view.View;
import android.view.View.OnDragListener;
import android.widget.TextView;
public class DropButton extends TextView implements OnDragListener {
private OnDropListener mListener;
public DropButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// TODO: Don't do this, instead make this view the right size...
((View) getParent()).setOnDragListener(this);
}
public void setOnDropListener(OnDropListener listener) {
mListener = listener;
}
private void setHovering(boolean hovering) {
setAlpha(hovering ? .3f : 1);
}
@Override
public boolean onDrag(View v, DragEvent event) {
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_ENTERED:
setHovering(true);
break;
case DragEvent.ACTION_DROP:
if (mListener != null) {
mListener.onDrop(this, event.getClipData());
}
case DragEvent.ACTION_DRAG_EXITED:
case DragEvent.ACTION_DRAG_ENDED:
setHovering(false);
break;
}
return true;
}
public interface OnDropListener {
void onDrop(View v, ClipData data);
}
}

View File

@@ -1,49 +0,0 @@
/*
* 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.animation.AnimatorInflater;
import android.content.Context;
import android.graphics.Outline;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.widget.ImageView;
import com.android.systemui.R;
public class FloatingActionButton extends ImageView {
public FloatingActionButton(Context context, AttributeSet attrs) {
super(context, attrs);
setScaleType(ScaleType.CENTER);
setStateListAnimator(AnimatorInflater.loadStateListAnimator(context, R.anim.fab_elevation));
setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setOval(0, 0, getWidth(), getHeight());
}
});
setClipToOutline(true);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
invalidateOutline();
}
}

View File

@@ -1,184 +0,0 @@
/*
* 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.ClipData;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.DragEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.LinearLayout;
import com.android.systemui.R;
import com.android.systemui.qs.PagedTileLayout;
import com.android.systemui.qs.PagedTileLayout.TilePage;
import com.android.systemui.qs.QSPanel.QSTileLayout;
import com.android.systemui.qs.QSPanel.TileRecord;
import com.android.systemui.qs.QSTile;
import java.util.ArrayList;
/**
* Similar to PagedTileLayout, except that instead of pages it lays them out
* vertically and expects to be inside a ScrollView.
* @see CustomQSPanel
*/
public class NonPagedTileLayout extends LinearLayout implements QSTileLayout, OnTouchListener {
private final ArrayList<TilePage> mPages = new ArrayList<>();
private final ArrayList<TileRecord> mTiles = new ArrayList<TileRecord>();
private CustomQSPanel mPanel;
private final Rect mHitRect = new Rect();
private ClipData mCurrentClip;
private View mCurrentView;
public NonPagedTileLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
TilePage page = (PagedTileLayout.TilePage) findViewById(R.id.tile_page);
page.setMaxRows(3 /* First page only gets 3 */);
mPages.add(page);
}
public void setCustomQsPanel(CustomQSPanel qsPanel) {
mPanel = qsPanel;
}
@Override
public void addTile(TileRecord record) {
mTiles.add(record);
distributeTiles();
if (record.tileView.getTag() == record.tile) {
return;
}
record.tileView.setTag(record.tile);
record.tileView.setVisibility(View.VISIBLE);
record.tileView.init(null, null);
record.tileView.setOnTouchListener(this);
if (mCurrentClip != null && mCurrentClip.getItemAt(0)
.getText().toString().equals(record.tile.getTileSpec())) {
record.tileView.setAlpha(.3f);
mCurrentView = record.tileView;
}
}
@Override
public void removeTile(TileRecord tile) {
if (mTiles.remove(tile)) {
distributeTiles();
}
}
private void distributeTiles() {
final int NP = mPages.size();
for (int i = 0; i < NP; i++) {
mPages.get(i).removeAllViews();
}
int index = 0;
final int NT = mTiles.size();
for (int i = 0; i < NT; i++) {
TileRecord tile = mTiles.get(i);
mPages.get(index).addTile(tile);
// Keep everything in one layout for now.
if (false && mPages.get(index).isFull()) {
if (++index == mPages.size()) {
LayoutInflater inflater = LayoutInflater.from(mContext);
inflater.inflate(R.layout.horizontal_divider, this);
mPages.add((TilePage) inflater.inflate(R.layout.qs_paged_page, this, false));
addView(mPages.get(mPages.size() - 1));
}
}
}
}
@Override
public int getOffsetTop(TileRecord tile) {
// No touch feedback, so this isn't required.
return 0;
}
@Override
public boolean updateResources() {
return false;
}
@Override
public boolean onDragEvent(DragEvent event) {
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_LOCATION:
float x = event.getX();
float y = event.getY();
final int NP = mPages.size();
for (int i = 0; i < NP; i++) {
TilePage page = mPages.get(i);
if (contains(page, x, y)) {
x -= page.getLeft();
y -= page.getTop();
final int NC = page.getChildCount();
for (int j = 0; j < NC; j++) {
View child = page.getChildAt(j);
if (contains(child, x, y)) {
mPanel.tileSelected((QSTile<?>) child.getTag(), mCurrentClip);
}
}
break;
}
}
break;
case DragEvent.ACTION_DRAG_ENDED:
onDragEnded();
break;
}
return true;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Stash the current tiles, in case the drop is on info, that we can restore
// the previous state.
mPanel.stashCurrentTiles();
mCurrentView = v;
mCurrentClip = mPanel.getClip((QSTile<?>) v.getTag());
View.DragShadowBuilder shadow = new View.DragShadowBuilder(v);
((View) getParent().getParent()).startDrag(mCurrentClip, shadow, null, 0);
v.setAlpha(.3f);
return true;
}
return false;
}
public void onDragEnded() {
mCurrentView.setAlpha(1f);
mCurrentView = null;
mCurrentClip = null;
}
private boolean contains(View v, float x, float y) {
v.getHitRect(mHitRect);
return mHitRect.contains((int) x, (int) y);
}
}

View File

@@ -16,38 +16,25 @@
package com.android.systemui.qs.customize;
import android.animation.Animator;
import android.content.ClipData;
import android.animation.Animator.AnimatorListener;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnDismissListener;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
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.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.qs.QSTile;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
import com.android.systemui.statusbar.phone.QSTileHost;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import java.util.ArrayList;
import java.util.List;
/**
* Allows full-screen customization of QS, through show() and hide().
@@ -55,26 +42,19 @@ import java.util.ArrayList;
* This adds itself to the status bar window, so it can appear on top of quick settings and
* *someday* do fancy animations to get into/out of it.
*/
public class QSCustomizer extends LinearLayout implements OnMenuItemClickListener, Callback,
OnDropListener, OnClickListener, Animator.AnimatorListener, TileSelectedListener,
OnCancelListener, OnDismissListener {
public class QSCustomizer extends LinearLayout implements AnimatorListener, OnClickListener {
private static final int MENU_SAVE = Menu.FIRST;
private static final int MENU_RESET = Menu.FIRST + 1;
private final QSDetailClipper mClipper;
private PhoneStatusBar mPhoneStatusBar;
private Toolbar mToolbar;
private ViewGroup mDragButtons;
private CustomQSPanel mQsPanel;
private boolean isShown;
private DropButton mInfoButton;
private DropButton mRemoveButton;
private FloatingActionButton mFab;
private SystemUIDialog mDialog;
private QSTileHost mHost;
private RecyclerView mRecyclerView;
private TileAdapter mTileAdapter;
private View mClose;
private View mSave;
private View mReset;
public QSCustomizer(Context context, AttributeSet attrs) {
super(new ContextThemeWrapper(context, android.R.style.Theme_Material), attrs);
@@ -83,59 +63,42 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene
public void setHost(QSTileHost host) {
mHost = host;
mHost.addCallback(this);
mPhoneStatusBar = host.getPhoneStatusBar();
mQsPanel.setTiles(mHost.getTiles());
mQsPanel.setHost(mHost);
mQsPanel.setSavedTiles();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mToolbar = (Toolbar) findViewById(com.android.internal.R.id.action_bar);
TypedValue value = new TypedValue();
mContext.getTheme().resolveAttribute(android.R.attr.homeAsUpIndicator, value, true);
mToolbar.setNavigationIcon(
getResources().getDrawable(R.drawable.ic_close_white, mContext.getTheme()));
mToolbar.setNavigationOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
hide(0, 0);
}
});
mToolbar.setOnMenuItemClickListener(this);
mToolbar.getMenu().add(Menu.NONE, MENU_SAVE, 0, mContext.getString(R.string.save))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
mToolbar.getMenu().add(Menu.NONE, MENU_RESET, 0,
mContext.getString(com.android.internal.R.string.reset));
mClose = findViewById(R.id.close);
mSave = findViewById(R.id.save);
mReset = findViewById(R.id.reset);
mClose.setOnClickListener(this);
mSave.setOnClickListener(this);
mReset.setOnClickListener(this);
mQsPanel = (CustomQSPanel) findViewById(R.id.quick_settings_panel);
mDragButtons = (ViewGroup) findViewById(R.id.drag_buttons);
setDragging(false);
mInfoButton = (DropButton) findViewById(R.id.info_button);
mInfoButton.setOnDropListener(this);
mRemoveButton = (DropButton) findViewById(R.id.remove_button);
mRemoveButton.setOnDropListener(this);
mFab = (FloatingActionButton) findViewById(R.id.fab);
mFab.setImageResource(R.drawable.ic_add);
mFab.setOnClickListener(this);
mRecyclerView = (RecyclerView) findViewById(android.R.id.list);
mTileAdapter = new TileAdapter(getContext());
mRecyclerView.setAdapter(mTileAdapter);
new ItemTouchHelper(mTileAdapter.getCallback()).attachToRecyclerView(mRecyclerView);
GridLayoutManager layout = new GridLayoutManager(getContext(), 3);
layout.setSpanSizeLookup(mTileAdapter.getSizeLookup());
mRecyclerView.setLayoutManager(layout);
mRecyclerView.addItemDecoration(mTileAdapter.getItemDecoration());
DefaultItemAnimator animator = new DefaultItemAnimator();
animator.setMoveDuration(TileAdapter.MOVE_DURATION);
mRecyclerView.setItemAnimator(animator);
}
public void show(int x, int y) {
isShown = true;
mQsPanel.setSavedTiles();
mPhoneStatusBar.getStatusBarWindow().addView(this);
mQsPanel.setListening(true);
setTileSpecs();
mClipper.animateCircularClip(x, y, true, this);
new TileQueryHelper(mContext, mHost).setListener(mTileAdapter);
}
public void hide(int x, int y) {
isShown = false;
mQsPanel.setListening(false);
mClipper.animateCircularClip(x, y, false, this);
}
@@ -149,108 +112,34 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene
for (String tile : defTiles.split(",")) {
tiles.add(tile);
}
mQsPanel.setTiles(tiles);
mTileAdapter.setTileSpecs(tiles);
}
private void setDragging(boolean dragging) {
mToolbar.setVisibility(!dragging ? View.VISIBLE : View.INVISIBLE);
private void setTileSpecs() {
List<String> specs = new ArrayList<>();
for (QSTile tile : mHost.getTiles()) {
specs.add(tile.getTileSpec());
}
mTileAdapter.setTileSpecs(specs);
}
private void save() {
Log.d("CustomQSPanel", "Save!");
mQsPanel.saveCurrentTiles();
// TODO: At save button.
hide(0, 0);
}
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case MENU_SAVE:
Log.d("CustomQSPanel", "Save...");
save();
break;
case MENU_RESET:
reset();
break;
}
return true;
}
@Override
public void onTileSelected(String spec) {
if (mDialog != null) {
mQsPanel.addTile(spec);
mDialog.dismiss();
}
}
@Override
public void onTilesChanged() {
mQsPanel.setTiles(mHost.getTiles());
}
public boolean onDragEvent(DragEvent event) {
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
setDragging(true);
break;
case DragEvent.ACTION_DRAG_ENDED:
setDragging(false);
break;
}
return true;
}
public void onDrop(View v, ClipData data) {
if (v == mRemoveButton) {
mQsPanel.remove(mQsPanel.getSpec(data));
} else if (v == mInfoButton) {
mQsPanel.unstashTiles();
SystemUIDialog dialog = new SystemUIDialog(mContext);
dialog.setTitle(mQsPanel.getSpec(data));
dialog.setPositiveButton(R.string.ok, null);
dialog.show();
}
mTileAdapter.saveSpecs(mHost);
hide((int) mSave.getX() + mSave.getWidth() / 2, (int) mSave.getY() + mSave.getHeight() / 2);
}
@Override
public void onClick(View v) {
if (mFab == v) {
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, mQsPanel.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);
if (v == mClose) {
hide((int) mClose.getX() + mClose.getWidth() / 2,
(int) mClose.getY() + mClose.getHeight() / 2);
} else if (v == mSave) {
save();
} else if (v == mReset) {
reset();
}
}
@Override
public void onDismiss(DialogInterface dialog) {
mDialog = null;
}
@Override
public void onCancel(DialogInterface dialog) {
mDialog = null;
}
@Override
public void onAnimationEnd(Animator animation) {
if (!isShown) {
@@ -274,4 +163,4 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene
public void onAnimationRepeat(Animator animation) {
// Don't care.
}
}
}

View File

@@ -1,262 +1,293 @@
/*
* Copyright (C) 2015 The Android Open Source Project
* 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
* 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.
* 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.service.quicksettings.TileService;
import android.util.Log;
import android.graphics.Canvas;
import android.graphics.drawable.ColorDrawable;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ItemDecoration;
import android.support.v7.widget.RecyclerView.State;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.support.v7.widget.helper.ItemTouchHelper.Callback;
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 android.widget.FrameLayout;
import com.android.systemui.R;
import com.android.systemui.qs.QSTile;
import com.android.systemui.qs.QSTile.Icon;
import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.qs.QSIconView;
import com.android.systemui.qs.QSTileView;
import com.android.systemui.qs.customize.TileAdapter.Holder;
import com.android.systemui.qs.customize.TileQueryHelper.TileInfo;
import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener;
import com.android.systemui.statusbar.phone.QSTileHost;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
public class TileAdapter extends BaseAdapter {
public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileStateListener {
private static final String TAG = "TileAdapter";
private static final long DRAG_LENGTH = 100;
private static final float DRAG_SCALE = 1.2f;
public static final long MOVE_DURATION = 150;
private static final int TYPE_TILE = 0;
private static final int TYPE_EDIT = 1;
private final ArrayList<TileGroup> mGroups = new ArrayList<>();
private final Context mContext;
private TileSelectedListener mListener;
private ArrayList<String> mCurrentTiles;
private final List<TileInfo> mTiles = new ArrayList<>();
private int mDividerIndex;
private List<String> mCurrentSpecs;
private List<TileInfo> mOtherTiles;
private List<TileInfo> mAllTiles;
public TileAdapter(Context context, Collection<QSTile<?>> currentTiles, QSTileHost host) {
private Holder mCurrentDrag;
public TileAdapter(Context context) {
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);
boolean hasColorMod = host.getDisplayController().isEnabled();
String possible = mContext.getString(R.string.quick_settings_tiles_default)
+ ",hotspot,inversion,saver" + (hasColorMod ? ",colors" : "");
String[] possibleTiles = possible.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)) {
Log.d(TAG, "Skipping " + spec);
continue;
}
Log.d(TAG, "Trying " + spec);
final QSTile<?> tile = host.createTile(spec);
if (tile == null) {
continue;
}
// 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);
setHasStableIds(true);
}
@Override
public long getItemId(int position) {
return position;
return mTiles.get(position) != null ? mAllTiles.indexOf(mTiles.get(position)) : -1;
}
public Callback getCallback() {
return mCallbacks;
}
public ItemDecoration getItemDecoration() {
return mDecoration;
}
public void saveSpecs(QSTileHost host) {
List<String> newSpecs = new ArrayList<>();
for (int i = 0; mTiles.get(i) != null; i++) {
newSpecs.add(mTiles.get(i).spec);
}
host.changeTiles(mCurrentSpecs, newSpecs);
setTileSpecs(newSpecs);
}
public void setTileSpecs(List<String> currentSpecs) {
mCurrentSpecs = currentSpecs;
recalcSpecs();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
return mGroups.get(position).getView(mContext, convertView, parent, mListener);
public void onTilesChanged(List<TileInfo> tiles) {
mAllTiles = tiles;
recalcSpecs();
}
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 recalcSpecs() {
if (mCurrentSpecs == null || mAllTiles == null) {
return;
}
private void addTile(String spec, Drawable icon, CharSequence label) {
TileInfo info = new TileInfo();
info.label = label;
info.drawable = icon;
info.spec = spec;
mTiles.add(info);
mOtherTiles = new ArrayList<TileInfo>(mAllTiles);
mTiles.clear();
for (int i = 0; i < mCurrentSpecs.size(); i++) {
mTiles.add(getAndRemoveOther(mCurrentSpecs.get(i)));
}
mTiles.add(null);
mTiles.addAll(mOtherTiles);
mDividerIndex = mTiles.indexOf(null);
notifyDataSetChanged();
}
private void addTile(String spec, Icon icon, CharSequence label, Context context) {
addTile(spec, icon != null ? icon.getDrawable(context) : null, 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);
private TileInfo getAndRemoveOther(String s) {
for (int i = 0; i < mOtherTiles.size(); i++) {
if (mOtherTiles.get(i).spec.equals(s)) {
return mOtherTiles.remove(i);
}
((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();
}
return null;
}
@Override
public int getItemViewType(int position) {
if (mTiles.get(position) == null) {
return TYPE_EDIT;
}
return TYPE_TILE;
}
@Override
public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
final Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
if (viewType == 1) {
return new Holder(inflater.inflate(R.layout.qs_customize_divider, parent, false));
}
FrameLayout frame = (FrameLayout) inflater.inflate(R.layout.qs_customize_tile_frame, parent,
false);
frame.addView(new QSTileView(context, new QSIconView(context)));
return new Holder(frame);
}
@Override
public int getItemCount() {
return mTiles.size();
}
@Override
public void onBindViewHolder(Holder holder, int position) {
if (holder.getItemViewType() == TYPE_EDIT) return;
TileInfo info = mTiles.get(position);
holder.mTileView.onStateChanged(info.state);
}
public SpanSizeLookup getSizeLookup() {
return mSizeLookup;
}
public class Holder extends ViewHolder {
private QSTileView mTileView;
public Holder(View itemView) {
super(itemView);
if (itemView instanceof FrameLayout) {
mTileView = (QSTileView) ((FrameLayout) itemView).getChildAt(0);
}
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);
public void startDrag() {
itemView.animate()
.setDuration(DRAG_LENGTH)
.scaleX(DRAG_SCALE)
.scaleY(DRAG_SCALE);
mTileView.findViewById(R.id.tile_label).animate()
.setDuration(DRAG_LENGTH)
.alpha(0);
}
public void stopDrag() {
itemView.animate()
.setDuration(DRAG_LENGTH)
.scaleX(1)
.scaleY(1);
mTileView.findViewById(R.id.tile_label).animate()
.setDuration(DRAG_LENGTH)
.alpha(1);
}
}
private static class TileInfo {
private String spec;
private Drawable drawable;
private CharSequence label;
}
private class QueryTilesTask extends AsyncTask<Void, Void, Collection<TileGroup>> {
private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() {
@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(TileService.ACTION_QS_TILE), 0);
for (ResolveInfo info : services) {
String packageName = info.serviceInfo.packageName;
ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);
String spec = CustomTile.toSpec(componentName);
if (mCurrentTiles.contains(spec)) {
public int getSpanSize(int position) {
return getItemViewType(position) == TYPE_EDIT ? 3 : 1;
}
};
private final ItemDecoration mDecoration = new ItemDecoration() {
// TODO: Move this to resource.
private final ColorDrawable mDrawable = new ColorDrawable(0xff384248);
@Override
public void onDraw(Canvas c, RecyclerView parent, State state) {
super.onDraw(c, parent, state);
final int childCount = parent.getChildCount();
final int width = parent.getWidth();
final int bottom = parent.getBottom();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final ViewHolder holder = parent.getChildViewHolder(child);
if (holder.getAdapterPosition() < mDividerIndex) {
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);
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
.getLayoutParams();
final int top = child.getTop() + params.topMargin +
Math.round(ViewCompat.getTranslationY(child));
// Draw full width, in case there aren't tiles all the way across.
mDrawable.setBounds(0, top, width, bottom);
mDrawable.draw(c);
break;
}
return pkgMap.values();
}
};
private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() {
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
protected void onPostExecute(Collection<TileGroup> result) {
mGroups.addAll(result);
notifyDataSetChanged();
public boolean isItemViewSwipeEnabled() {
return false;
}
}
public interface TileSelectedListener {
void onTileSelected(String spec);
}
@Override
public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
super.onSelectedChanged(viewHolder, actionState);
if (mCurrentDrag != null) {
mCurrentDrag.stopDrag();
}
if (viewHolder != null) {
mCurrentDrag = (Holder) viewHolder;
mCurrentDrag.startDrag();
}
}
@Override
public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
if (viewHolder.getItemViewType() == TYPE_EDIT) {
return makeMovementFlags(0, 0);
}
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.RIGHT
| ItemTouchHelper.LEFT;
return makeMovementFlags(dragFlags, 0);
}
@Override
public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) {
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
if (to > mDividerIndex) {
if (from < mDividerIndex) {
to = mDividerIndex;
} else {
return false;
}
}
if (target.getItemViewType() == TYPE_EDIT && from < mDividerIndex) {
to++;
}
move(from, to, mTiles);
mDividerIndex = mTiles.indexOf(null);
notifyItemMoved(from, to);
return true;
}
private <T> void move(int from, int to, List<T> list) {
list.add(from > to ? to : to + 1, list.get(from));
list.remove(from > to ? from + 1 : from);
}
@Override
public void onSwiped(ViewHolder viewHolder, int direction) {
}
};
}

View File

@@ -0,0 +1,151 @@
/*
* 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.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.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.service.quicksettings.TileService;
import com.android.systemui.R;
import com.android.systemui.qs.QSTile;
import com.android.systemui.qs.QSTile.DrawableIcon;
import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.statusbar.phone.QSTileHost;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class TileQueryHelper {
private static final String TAG = "TileQueryHelper";
private final ArrayList<TileInfo> mTiles = new ArrayList<>();
private final Context mContext;
private TileStateListener mListener;
public TileQueryHelper(Context context, QSTileHost host) {
mContext = context;
addSystemTiles(host);
// TODO: Live?
}
private void addSystemTiles(QSTileHost host) {
boolean hasColorMod = host.getDisplayController().isEnabled();
String possible = mContext.getString(R.string.quick_settings_tiles_default)
+ ",hotspot,inversion,saver" + (hasColorMod ? ",colors" : "");
String[] possibleTiles = possible.split(",");
final Handler qsHandler = new Handler(host.getLooper());
final Handler mainHandler = new Handler(Looper.getMainLooper());
for (int i = 0; i < possibleTiles.length; i++) {
final String spec = possibleTiles[i];
final QSTile<?> tile = host.createTile(spec);
if (tile == null) {
continue;
}
tile.setListening(true);
tile.clearState();
tile.refreshState();
tile.setListening(false);
qsHandler.post(new Runnable() {
@Override
public void run() {
final QSTile.State state = tile.newTileState();
tile.getState().copyTo(state);
mainHandler.post(new Runnable() {
@Override
public void run() {
addTile(spec, state, mTiles);
mListener.onTilesChanged(mTiles);
}
});
}
});
}
qsHandler.post(new Runnable() {
@Override
public void run() {
new QueryTilesTask().execute();
}
});
}
public void setListener(TileStateListener listener) {
mListener = listener;
}
private static void addTile(String spec, QSTile.State state, List<TileInfo> tiles) {
TileInfo info = new TileInfo();
info.state = state;
info.spec = spec;
tiles.add(info);
}
private static void addTile(String spec, Drawable drawable, CharSequence label, Context context,
List<TileInfo> tiles) {
QSTile.State state = new QSTile.State();
state.label = label;
state.contentDescription = label;
state.icon = new DrawableIcon(drawable);
addTile(spec, state, tiles);
}
public static class TileInfo {
public String spec;
public QSTile.State state;
}
private class QueryTilesTask extends AsyncTask<Void, Void, Collection<TileInfo>> {
@Override
protected Collection<TileInfo> doInBackground(Void... params) {
List<TileInfo> tiles = new ArrayList<>();
PackageManager pm = mContext.getPackageManager();
List<ResolveInfo> services = pm.queryIntentServicesAsUser(
new Intent(TileService.ACTION_QS_TILE), 0, ActivityManager.getCurrentUser());
for (ResolveInfo info : services) {
String packageName = info.serviceInfo.packageName;
ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);
String spec = CustomTile.toSpec(componentName);
Drawable icon = info.serviceInfo.loadIcon(pm);
if (icon != null) {
icon.mutate();
icon.setTint(mContext.getColor(android.R.color.white));
}
CharSequence label = info.serviceInfo.loadLabel(pm);
addTile(spec, icon, label != null ? label.toString() : "null", mContext, tiles);
}
return tiles;
}
@Override
protected void onPostExecute(Collection<TileInfo> result) {
mTiles.addAll(result);
mListener.onTilesChanged(mTiles);
}
}
public interface TileStateListener {
void onTilesChanged(List<TileInfo> tiles);
}
}

View File

@@ -151,7 +151,7 @@ public class CustomTile extends QSTile<QSTile.State> {
}
@Override
protected State newTileState() {
public State newTileState() {
return new State();
}

View File

@@ -51,7 +51,7 @@ public class AirplaneModeTile extends QSTile<QSTile.BooleanState> {
}
@Override
protected BooleanState newTileState() {
public BooleanState newTileState() {
return new BooleanState();
}

View File

@@ -54,7 +54,7 @@ public class BatteryTile extends QSTile<QSTile.State> implements BatteryControll
}
@Override
protected State newTileState() {
public State newTileState() {
return new QSTile.State();
}

View File

@@ -56,7 +56,7 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> {
}
@Override
protected BooleanState newTileState() {
public BooleanState newTileState() {
return new BooleanState();
}

View File

@@ -60,7 +60,7 @@ public class CastTile extends QSTile<QSTile.BooleanState> {
}
@Override
protected BooleanState newTileState() {
public BooleanState newTileState() {
return new BooleanState();
}

View File

@@ -54,7 +54,7 @@ public class CellularTile extends QSTile<QSTile.SignalState> {
}
@Override
protected SignalState newTileState() {
public SignalState newTileState() {
return new SignalState();
}

View File

@@ -54,7 +54,7 @@ public class ColorInversionTile extends QSTile<QSTile.BooleanState> {
}
@Override
protected BooleanState newTileState() {
public BooleanState newTileState() {
return new BooleanState();
}

View File

@@ -30,7 +30,7 @@ public class DataSaverTile extends QSTile<QSTile.BooleanState> implements
}
@Override
protected BooleanState newTileState() {
public BooleanState newTileState() {
return new BooleanState();
}

View File

@@ -94,7 +94,7 @@ public class DndTile extends QSTile<QSTile.BooleanState> {
}
@Override
protected BooleanState newTileState() {
public BooleanState newTileState() {
return new BooleanState();
}

View File

@@ -47,7 +47,7 @@ public class FlashlightTile extends QSTile<QSTile.BooleanState> implements
}
@Override
protected BooleanState newTileState() {
public BooleanState newTileState() {
return new BooleanState();
}

View File

@@ -44,7 +44,7 @@ public class HotspotTile extends QSTile<QSTile.BooleanState> {
}
@Override
protected BooleanState newTileState() {
public BooleanState newTileState() {
return new BooleanState();
}

View File

@@ -75,7 +75,7 @@ public class IntentTile extends QSTile<QSTile.State> {
}
@Override
protected State newTileState() {
public State newTileState() {
return new State();
}

View File

@@ -45,7 +45,7 @@ public class LocationTile extends QSTile<QSTile.BooleanState> {
}
@Override
protected BooleanState newTileState() {
public BooleanState newTileState() {
return new BooleanState();
}

View File

@@ -46,7 +46,7 @@ public class RotationLockTile extends QSTile<QSTile.BooleanState> {
}
@Override
protected BooleanState newTileState() {
public BooleanState newTileState() {
return new BooleanState();
}

View File

@@ -37,7 +37,7 @@ public class UserTile extends QSTile<QSTile.State> implements UserInfoController
}
@Override
protected State newTileState() {
public State newTileState() {
return new QSTile.State();
}

View File

@@ -60,7 +60,7 @@ public class WifiTile extends QSTile<QSTile.SignalState> {
}
@Override
protected SignalState newTileState() {
public SignalState newTileState() {
return new SignalState();
}

View File

@@ -53,7 +53,7 @@ public class WorkModeTile extends QSTile<QSTile.BooleanState> {
}
@Override
protected BooleanState newTileState() {
public BooleanState newTileState() {
return new BooleanState();
}

View File

@@ -56,7 +56,7 @@ public class ColorMatrixTile extends QSTile<QSTile.State> implements DisplayCont
}
@Override
protected State newTileState() {
public State newTileState() {
return new State();
}