/* * 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.tuner; import android.app.ActivityManager; import android.app.AlertDialog; import android.app.Fragment; import android.content.ClipData; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.provider.Settings.Secure; import android.text.TextUtils; import android.util.Log; import android.view.DragEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnDragListener; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ScrollView; import com.android.internal.logging.MetricsLogger; import com.android.systemui.R; import com.android.systemui.qs.QSPanel; import com.android.systemui.qs.QSTile; import com.android.systemui.qs.QSTile.Host.Callback; import com.android.systemui.qs.QSTile.ResourceIcon; import com.android.systemui.qs.QSTileView; import com.android.systemui.qs.tiles.IntentTile; import com.android.systemui.statusbar.phone.QSTileHost; import com.android.systemui.statusbar.policy.SecurityController; import java.util.ArrayList; import java.util.List; public class QsTuner extends Fragment implements Callback { private static final String TAG = "QsTuner"; private static final int MENU_RESET = Menu.FIRST; private DraggableQsPanel mQsPanel; private CustomHost mTileHost; private FrameLayout mDropTarget; private ScrollView mScrollRoot; private FrameLayout mAddTarget; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { menu.add(0, MENU_RESET, 0, com.android.internal.R.string.reset); } public void onResume() { super.onResume(); MetricsLogger.visibility(getContext(), MetricsLogger.TUNER_QS, true); } public void onPause() { super.onPause(); MetricsLogger.visibility(getContext(), MetricsLogger.TUNER_QS, false); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case MENU_RESET: mTileHost.reset(); break; case android.R.id.home: getFragmentManager().popBackStack(); break; } return super.onOptionsItemSelected(item); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mScrollRoot = (ScrollView) inflater.inflate(R.layout.tuner_qs, container, false); mQsPanel = new DraggableQsPanel(getContext()); mTileHost = new CustomHost(getContext()); mTileHost.setCallback(this); mQsPanel.setTiles(mTileHost.getTiles()); mQsPanel.setHost(mTileHost); mQsPanel.refreshAllTiles(); ((ViewGroup) mScrollRoot.findViewById(R.id.all_details)).addView(mQsPanel, 0); mDropTarget = (FrameLayout) mScrollRoot.findViewById(R.id.remove_target); setupDropTarget(); mAddTarget = (FrameLayout) mScrollRoot.findViewById(R.id.add_target); setupAddTarget(); return mScrollRoot; } @Override public void onDestroyView() { mTileHost.destroy(); super.onDestroyView(); } private void setupDropTarget() { QSTileView tileView = new QSTileView(getContext()); QSTile.State state = new QSTile.State(); state.visible = true; state.icon = ResourceIcon.get(R.drawable.ic_delete); state.label = getString(com.android.internal.R.string.delete); tileView.onStateChanged(state); mDropTarget.addView(tileView); mDropTarget.setVisibility(View.GONE); new DragHelper(tileView, new DropListener() { @Override public void onDrop(String sourceText) { mTileHost.remove(sourceText); } }); } private void setupAddTarget() { QSTileView tileView = new QSTileView(getContext()); QSTile.State state = new QSTile.State(); state.visible = true; state.icon = ResourceIcon.get(R.drawable.ic_add_circle_qs); state.label = getString(R.string.add_tile); tileView.onStateChanged(state); mAddTarget.addView(tileView); tileView.setClickable(true); tileView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mTileHost.showAddDialog(); } }); } public void onStartDrag() { mDropTarget.post(new Runnable() { @Override public void run() { mDropTarget.setVisibility(View.VISIBLE); mAddTarget.setVisibility(View.GONE); } }); } public void stopDrag() { mDropTarget.post(new Runnable() { @Override public void run() { mDropTarget.setVisibility(View.GONE); mAddTarget.setVisibility(View.VISIBLE); } }); } @Override public void onTilesChanged() { mQsPanel.setTiles(mTileHost.getTiles()); } private static int getLabelResource(String spec) { if (spec.equals("wifi")) return R.string.quick_settings_wifi_label; else if (spec.equals("bt")) return R.string.quick_settings_bluetooth_label; else if (spec.equals("inversion")) return R.string.quick_settings_inversion_label; else if (spec.equals("cell")) return R.string.quick_settings_cellular_detail_title; else if (spec.equals("airplane")) return R.string.airplane_mode; else if (spec.equals("dnd")) return R.string.quick_settings_dnd_label; else if (spec.equals("rotation")) return R.string.quick_settings_rotation_locked_label; else if (spec.equals("flashlight")) return R.string.quick_settings_flashlight_label; else if (spec.equals("location")) return R.string.quick_settings_location_label; else if (spec.equals("cast")) return R.string.quick_settings_cast_title; else if (spec.equals("hotspot")) return R.string.quick_settings_hotspot_label; return 0; } private static class CustomHost extends QSTileHost { public CustomHost(Context context) { super(context, null, null, null, null, null, null, null, null, null, null, null, new BlankSecurityController()); } @Override protected QSTile<?> createTile(String tileSpec) { return new DraggableTile(this, tileSpec); } public void replace(String oldTile, String newTile) { if (oldTile.equals(newTile)) { return; } MetricsLogger.action(getContext(), MetricsLogger.TUNER_QS_REORDER, oldTile + "," + newTile); List<String> order = new ArrayList<>(mTileSpecs); int index = order.indexOf(oldTile); if (index < 0) { Log.e(TAG, "Can't find " + oldTile); return; } order.remove(newTile); order.add(index, newTile); setTiles(order); } public void remove(String tile) { MetricsLogger.action(getContext(), MetricsLogger.TUNER_QS_REMOVE, tile); List<String> tiles = new ArrayList<>(mTileSpecs); tiles.remove(tile); setTiles(tiles); } public void add(String tile) { MetricsLogger.action(getContext(), MetricsLogger.TUNER_QS_ADD, tile); List<String> tiles = new ArrayList<>(mTileSpecs); tiles.add(tile); setTiles(tiles); } public void reset() { Secure.putStringForUser(getContext().getContentResolver(), TILES_SETTING, "default", ActivityManager.getCurrentUser()); } private void setTiles(List<String> tiles) { Secure.putStringForUser(getContext().getContentResolver(), TILES_SETTING, TextUtils.join(",", tiles), ActivityManager.getCurrentUser()); } public void showAddDialog() { List<String> tiles = mTileSpecs; int numBroadcast = 0; for (int i = 0; i < tiles.size(); i++) { if (tiles.get(i).startsWith(IntentTile.PREFIX)) { numBroadcast++; } } String[] defaults = getContext().getString(R.string.quick_settings_tiles_default).split(","); final String[] available = new String[defaults.length + 1 - (tiles.size() - numBroadcast)]; final String[] availableTiles = new String[available.length]; int index = 0; for (int i = 0; i < defaults.length; i++) { if (tiles.contains(defaults[i])) { continue; } int resource = getLabelResource(defaults[i]); if (resource != 0) { availableTiles[index] = defaults[i]; available[index++] = getContext().getString(resource); } else { availableTiles[index] = defaults[i]; available[index++] = defaults[i]; } } available[index++] = getContext().getString(R.string.broadcast_tile); new AlertDialog.Builder(getContext()) .setTitle(R.string.add_tile) .setItems(available, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { if (which < available.length - 1) { add(availableTiles[which]); } else { showBroadcastTileDialog(); } } }).show(); } public void showBroadcastTileDialog() { final EditText editText = new EditText(getContext()); new AlertDialog.Builder(getContext()) .setTitle(R.string.broadcast_tile) .setView(editText) .setNegativeButton(android.R.string.cancel, null) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { String action = editText.getText().toString(); if (isValid(action)) { add(IntentTile.PREFIX + action + ')'); } } }).show(); } private boolean isValid(String action) { for (int i = 0; i < action.length(); i++) { char c = action.charAt(i); if (!Character.isAlphabetic(c) && !Character.isDigit(c) && c != '.') { return false; } } return true; } private static class BlankSecurityController implements SecurityController { @Override public boolean hasDeviceOwner() { return false; } @Override public boolean hasProfileOwner() { return false; } @Override public String getDeviceOwnerName() { return null; } @Override public String getProfileOwnerName() { return null; } @Override public boolean isVpnEnabled() { return false; } @Override public String getPrimaryVpnName() { return null; } @Override public String getProfileVpnName() { return null; } @Override public void onUserSwitched(int newUserId) { } @Override public void addCallback(SecurityControllerCallback callback) { } @Override public void removeCallback(SecurityControllerCallback callback) { } } } private static class DraggableTile extends QSTile<QSTile.State> implements DropListener { private String mSpec; private QSTileView mView; protected DraggableTile(QSTile.Host host, String tileSpec) { super(host); Log.d(TAG, "Creating tile " + tileSpec); mSpec = tileSpec; } @Override public QSTileView createTileView(Context context) { mView = super.createTileView(context); return mView; } @Override public boolean supportsDualTargets() { return "wifi".equals(mSpec) || "bt".equals(mSpec); } @Override public void setListening(boolean listening) { } @Override protected QSTile.State newTileState() { return new QSTile.State(); } @Override protected void handleClick() { } @Override protected void handleUpdateState(QSTile.State state, Object arg) { state.visible = true; state.icon = ResourceIcon.get(getIcon()); state.label = getLabel(); } private String getLabel() { int resource = getLabelResource(mSpec); if (resource != 0) { return mContext.getString(resource); } if (mSpec.startsWith(IntentTile.PREFIX)) { int lastDot = mSpec.lastIndexOf('.'); if (lastDot >= 0) { return mSpec.substring(lastDot + 1, mSpec.length() - 1); } else { return mSpec.substring(IntentTile.PREFIX.length(), mSpec.length() - 1); } } return mSpec; } private int getIcon() { if (mSpec.equals("wifi")) return R.drawable.ic_qs_wifi_full_3; else if (mSpec.equals("bt")) return R.drawable.ic_qs_bluetooth_connected; else if (mSpec.equals("inversion")) return R.drawable.ic_invert_colors_enable; else if (mSpec.equals("cell")) return R.drawable.ic_qs_signal_full_3; else if (mSpec.equals("airplane")) return R.drawable.ic_signal_airplane_enable; else if (mSpec.equals("dnd")) return R.drawable.ic_qs_dnd_on; else if (mSpec.equals("rotation")) return R.drawable.ic_portrait_from_auto_rotate; else if (mSpec.equals("flashlight")) return R.drawable.ic_signal_flashlight_enable; else if (mSpec.equals("location")) return R.drawable.ic_signal_location_enable; else if (mSpec.equals("cast")) return R.drawable.ic_qs_cast_on; else if (mSpec.equals("hotspot")) return R.drawable.ic_hotspot_enable; return R.drawable.android; } @Override public int getMetricsCategory() { return 20000; } @Override public boolean equals(Object o) { if (o instanceof DraggableTile) { return mSpec.equals(((DraggableTile) o).mSpec); } return false; } @Override public void onDrop(String sourceText) { ((CustomHost) mHost).replace(mSpec, sourceText); } } private class DragHelper implements OnDragListener { private final View mView; private final DropListener mListener; public DragHelper(View view, DropListener dropListener) { mView = view; mListener = dropListener; mView.setOnDragListener(this); } @Override public boolean onDrag(View v, DragEvent event) { switch (event.getAction()) { case DragEvent.ACTION_DRAG_ENTERED: mView.setBackgroundColor(0x77ffffff); break; case DragEvent.ACTION_DRAG_ENDED: stopDrag(); case DragEvent.ACTION_DRAG_EXITED: mView.setBackgroundColor(0x0); break; case DragEvent.ACTION_DROP: stopDrag(); String text = event.getClipData().getItemAt(0).getText().toString(); mListener.onDrop(text); break; } return true; } } public interface DropListener { void onDrop(String sourceText); } private class DraggableQsPanel extends QSPanel implements OnTouchListener { public DraggableQsPanel(Context context) { super(context); mBrightnessView.setVisibility(View.GONE); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); for (TileRecord r : mRecords) { new DragHelper(r.tileView, (DraggableTile) r.tile); r.tileView.setTag(r.tile); r.tileView.setOnTouchListener(this); for (int i = 0; i < r.tileView.getChildCount(); i++) { r.tileView.getChildAt(i).setClickable(false); } } } @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: String tileSpec = (String) ((DraggableTile) v.getTag()).mSpec; ClipData data = ClipData.newPlainText(tileSpec, tileSpec); v.startDrag(data, new View.DragShadowBuilder(v), null, 0); onStartDrag(); return true; } return false; } } }