1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.systemui.tuner; 17 18 import android.app.ActivityManager; 19 import android.app.AlertDialog; 20 import android.app.Fragment; 21 import android.content.ClipData; 22 import android.content.Context; 23 import android.content.DialogInterface; 24 import android.os.Bundle; 25 import android.provider.Settings.Secure; 26 import android.text.TextUtils; 27 import android.util.Log; 28 import android.view.DragEvent; 29 import android.view.LayoutInflater; 30 import android.view.Menu; 31 import android.view.MenuInflater; 32 import android.view.MenuItem; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.View.OnClickListener; 36 import android.view.View.OnDragListener; 37 import android.view.View.OnTouchListener; 38 import android.view.ViewGroup; 39 import android.widget.EditText; 40 import android.widget.FrameLayout; 41 import android.widget.ScrollView; 42 43 import com.android.internal.logging.MetricsLogger; 44 import com.android.systemui.R; 45 import com.android.systemui.qs.QSPanel; 46 import com.android.systemui.qs.QSTile; 47 import com.android.systemui.qs.QSTile.Host.Callback; 48 import com.android.systemui.qs.QSTile.ResourceIcon; 49 import com.android.systemui.qs.QSTileView; 50 import com.android.systemui.qs.tiles.IntentTile; 51 import com.android.systemui.statusbar.phone.QSTileHost; 52 import com.android.systemui.statusbar.policy.SecurityController; 53 54 import java.util.ArrayList; 55 import java.util.List; 56 57 public class QsTuner extends Fragment implements Callback { 58 59 private static final String TAG = "QsTuner"; 60 61 private static final int MENU_RESET = Menu.FIRST; 62 63 private DraggableQsPanel mQsPanel; 64 private CustomHost mTileHost; 65 66 private FrameLayout mDropTarget; 67 68 private ScrollView mScrollRoot; 69 70 private FrameLayout mAddTarget; 71 72 @Override 73 public void onCreate(Bundle savedInstanceState) { 74 super.onCreate(savedInstanceState); 75 setHasOptionsMenu(true); 76 } 77 78 @Override 79 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 80 menu.add(0, MENU_RESET, 0, com.android.internal.R.string.reset); 81 } 82 83 public void onResume() { 84 super.onResume(); 85 MetricsLogger.visibility(getContext(), MetricsLogger.TUNER_QS, true); 86 } 87 88 public void onPause() { 89 super.onPause(); 90 MetricsLogger.visibility(getContext(), MetricsLogger.TUNER_QS, false); 91 } 92 93 @Override 94 public boolean onOptionsItemSelected(MenuItem item) { 95 switch (item.getItemId()) { 96 case MENU_RESET: 97 mTileHost.reset(); 98 break; 99 case android.R.id.home: 100 getFragmentManager().popBackStack(); 101 break; 102 } 103 return super.onOptionsItemSelected(item); 104 } 105 106 @Override 107 public View onCreateView(LayoutInflater inflater, ViewGroup container, 108 Bundle savedInstanceState) { 109 mScrollRoot = (ScrollView) inflater.inflate(R.layout.tuner_qs, container, false); 110 111 mQsPanel = new DraggableQsPanel(getContext()); 112 mTileHost = new CustomHost(getContext()); 113 mTileHost.setCallback(this); 114 mQsPanel.setTiles(mTileHost.getTiles()); 115 mQsPanel.setHost(mTileHost); 116 mQsPanel.refreshAllTiles(); 117 ((ViewGroup) mScrollRoot.findViewById(R.id.all_details)).addView(mQsPanel, 0); 118 119 mDropTarget = (FrameLayout) mScrollRoot.findViewById(R.id.remove_target); 120 setupDropTarget(); 121 mAddTarget = (FrameLayout) mScrollRoot.findViewById(R.id.add_target); 122 setupAddTarget(); 123 return mScrollRoot; 124 } 125 126 @Override 127 public void onDestroyView() { 128 mTileHost.destroy(); 129 super.onDestroyView(); 130 } 131 132 private void setupDropTarget() { 133 QSTileView tileView = new QSTileView(getContext()); 134 QSTile.State state = new QSTile.State(); 135 state.visible = true; 136 state.icon = ResourceIcon.get(R.drawable.ic_delete); 137 state.label = getString(com.android.internal.R.string.delete); 138 tileView.onStateChanged(state); 139 mDropTarget.addView(tileView); 140 mDropTarget.setVisibility(View.GONE); 141 new DragHelper(tileView, new DropListener() { 142 @Override 143 public void onDrop(String sourceText) { 144 mTileHost.remove(sourceText); 145 } 146 }); 147 } 148 149 private void setupAddTarget() { 150 QSTileView tileView = new QSTileView(getContext()); 151 QSTile.State state = new QSTile.State(); 152 state.visible = true; 153 state.icon = ResourceIcon.get(R.drawable.ic_add_circle_qs); 154 state.label = getString(R.string.add_tile); 155 tileView.onStateChanged(state); 156 mAddTarget.addView(tileView); 157 tileView.setClickable(true); 158 tileView.setOnClickListener(new OnClickListener() { 159 @Override 160 public void onClick(View v) { 161 mTileHost.showAddDialog(); 162 } 163 }); 164 } 165 166 public void onStartDrag() { 167 mDropTarget.post(new Runnable() { 168 @Override 169 public void run() { 170 mDropTarget.setVisibility(View.VISIBLE); 171 mAddTarget.setVisibility(View.GONE); 172 } 173 }); 174 } 175 176 public void stopDrag() { 177 mDropTarget.post(new Runnable() { 178 @Override 179 public void run() { 180 mDropTarget.setVisibility(View.GONE); 181 mAddTarget.setVisibility(View.VISIBLE); 182 } 183 }); 184 } 185 186 @Override 187 public void onTilesChanged() { 188 mQsPanel.setTiles(mTileHost.getTiles()); 189 } 190 191 private static int getLabelResource(String spec) { 192 if (spec.equals("wifi")) return R.string.quick_settings_wifi_label; 193 else if (spec.equals("bt")) return R.string.quick_settings_bluetooth_label; 194 else if (spec.equals("inversion")) return R.string.quick_settings_inversion_label; 195 else if (spec.equals("cell")) return R.string.quick_settings_cellular_detail_title; 196 else if (spec.equals("airplane")) return R.string.airplane_mode; 197 else if (spec.equals("dnd")) return R.string.quick_settings_dnd_label; 198 else if (spec.equals("rotation")) return R.string.quick_settings_rotation_locked_label; 199 else if (spec.equals("flashlight")) return R.string.quick_settings_flashlight_label; 200 else if (spec.equals("location")) return R.string.quick_settings_location_label; 201 else if (spec.equals("cast")) return R.string.quick_settings_cast_title; 202 else if (spec.equals("hotspot")) return R.string.quick_settings_hotspot_label; 203 return 0; 204 } 205 206 private static class CustomHost extends QSTileHost { 207 208 public CustomHost(Context context) { 209 super(context, null, null, null, null, null, null, null, null, null, 210 null, null, new BlankSecurityController()); 211 } 212 213 @Override 214 protected QSTile<?> createTile(String tileSpec) { 215 return new DraggableTile(this, tileSpec); 216 } 217 218 public void replace(String oldTile, String newTile) { 219 if (oldTile.equals(newTile)) { 220 return; 221 } 222 MetricsLogger.action(getContext(), MetricsLogger.TUNER_QS_REORDER, oldTile + "," 223 + newTile); 224 List<String> order = new ArrayList<>(mTileSpecs); 225 int index = order.indexOf(oldTile); 226 if (index < 0) { 227 Log.e(TAG, "Can't find " + oldTile); 228 return; 229 } 230 order.remove(newTile); 231 order.add(index, newTile); 232 setTiles(order); 233 } 234 235 public void remove(String tile) { 236 MetricsLogger.action(getContext(), MetricsLogger.TUNER_QS_REMOVE, tile); 237 List<String> tiles = new ArrayList<>(mTileSpecs); 238 tiles.remove(tile); 239 setTiles(tiles); 240 } 241 242 public void add(String tile) { 243 MetricsLogger.action(getContext(), MetricsLogger.TUNER_QS_ADD, tile); 244 List<String> tiles = new ArrayList<>(mTileSpecs); 245 tiles.add(tile); 246 setTiles(tiles); 247 } 248 249 public void reset() { 250 Secure.putStringForUser(getContext().getContentResolver(), 251 TILES_SETTING, "default", ActivityManager.getCurrentUser()); 252 } 253 254 private void setTiles(List<String> tiles) { 255 Secure.putStringForUser(getContext().getContentResolver(), TILES_SETTING, 256 TextUtils.join(",", tiles), ActivityManager.getCurrentUser()); 257 } 258 259 public void showAddDialog() { 260 List<String> tiles = mTileSpecs; 261 int numBroadcast = 0; 262 for (int i = 0; i < tiles.size(); i++) { 263 if (tiles.get(i).startsWith(IntentTile.PREFIX)) { 264 numBroadcast++; 265 } 266 } 267 String[] defaults = 268 getContext().getString(R.string.quick_settings_tiles_default).split(","); 269 final String[] available = new String[defaults.length + 1 270 - (tiles.size() - numBroadcast)]; 271 final String[] availableTiles = new String[available.length]; 272 int index = 0; 273 for (int i = 0; i < defaults.length; i++) { 274 if (tiles.contains(defaults[i])) { 275 continue; 276 } 277 int resource = getLabelResource(defaults[i]); 278 if (resource != 0) { 279 availableTiles[index] = defaults[i]; 280 available[index++] = getContext().getString(resource); 281 } else { 282 availableTiles[index] = defaults[i]; 283 available[index++] = defaults[i]; 284 } 285 } 286 available[index++] = getContext().getString(R.string.broadcast_tile); 287 new AlertDialog.Builder(getContext()) 288 .setTitle(R.string.add_tile) 289 .setItems(available, new DialogInterface.OnClickListener() { 290 public void onClick(DialogInterface dialog, int which) { 291 if (which < available.length - 1) { 292 add(availableTiles[which]); 293 } else { 294 showBroadcastTileDialog(); 295 } 296 } 297 }).show(); 298 } 299 300 public void showBroadcastTileDialog() { 301 final EditText editText = new EditText(getContext()); 302 new AlertDialog.Builder(getContext()) 303 .setTitle(R.string.broadcast_tile) 304 .setView(editText) 305 .setNegativeButton(android.R.string.cancel, null) 306 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 307 public void onClick(DialogInterface dialog, int which) { 308 String action = editText.getText().toString(); 309 if (isValid(action)) { 310 add(IntentTile.PREFIX + action + ')'); 311 } 312 } 313 }).show(); 314 } 315 316 private boolean isValid(String action) { 317 for (int i = 0; i < action.length(); i++) { 318 char c = action.charAt(i); 319 if (!Character.isAlphabetic(c) && !Character.isDigit(c) && c != '.') { 320 return false; 321 } 322 } 323 return true; 324 } 325 326 private static class BlankSecurityController implements SecurityController { 327 @Override 328 public boolean hasDeviceOwner() { 329 return false; 330 } 331 332 @Override 333 public boolean hasProfileOwner() { 334 return false; 335 } 336 337 @Override 338 public String getDeviceOwnerName() { 339 return null; 340 } 341 342 @Override 343 public String getProfileOwnerName() { 344 return null; 345 } 346 347 @Override 348 public boolean isVpnEnabled() { 349 return false; 350 } 351 352 @Override 353 public String getPrimaryVpnName() { 354 return null; 355 } 356 357 @Override 358 public String getProfileVpnName() { 359 return null; 360 } 361 362 @Override 363 public void onUserSwitched(int newUserId) { 364 } 365 366 @Override 367 public void addCallback(SecurityControllerCallback callback) { 368 } 369 370 @Override 371 public void removeCallback(SecurityControllerCallback callback) { 372 } 373 } 374 } 375 376 private static class DraggableTile extends QSTile<QSTile.State> 377 implements DropListener { 378 private String mSpec; 379 private QSTileView mView; 380 381 protected DraggableTile(QSTile.Host host, String tileSpec) { 382 super(host); 383 Log.d(TAG, "Creating tile " + tileSpec); 384 mSpec = tileSpec; 385 } 386 387 @Override 388 public QSTileView createTileView(Context context) { 389 mView = super.createTileView(context); 390 return mView; 391 } 392 393 @Override 394 public boolean supportsDualTargets() { 395 return "wifi".equals(mSpec) || "bt".equals(mSpec); 396 } 397 398 @Override 399 public void setListening(boolean listening) { 400 } 401 402 @Override 403 protected QSTile.State newTileState() { 404 return new QSTile.State(); 405 } 406 407 @Override 408 protected void handleClick() { 409 } 410 411 @Override 412 protected void handleUpdateState(QSTile.State state, Object arg) { 413 state.visible = true; 414 state.icon = ResourceIcon.get(getIcon()); 415 state.label = getLabel(); 416 } 417 418 private String getLabel() { 419 int resource = getLabelResource(mSpec); 420 if (resource != 0) { 421 return mContext.getString(resource); 422 } 423 if (mSpec.startsWith(IntentTile.PREFIX)) { 424 int lastDot = mSpec.lastIndexOf('.'); 425 if (lastDot >= 0) { 426 return mSpec.substring(lastDot + 1, mSpec.length() - 1); 427 } else { 428 return mSpec.substring(IntentTile.PREFIX.length(), mSpec.length() - 1); 429 } 430 } 431 return mSpec; 432 } 433 434 private int getIcon() { 435 if (mSpec.equals("wifi")) return R.drawable.ic_qs_wifi_full_3; 436 else if (mSpec.equals("bt")) return R.drawable.ic_qs_bluetooth_connected; 437 else if (mSpec.equals("inversion")) return R.drawable.ic_invert_colors_enable; 438 else if (mSpec.equals("cell")) return R.drawable.ic_qs_signal_full_3; 439 else if (mSpec.equals("airplane")) return R.drawable.ic_signal_airplane_enable; 440 else if (mSpec.equals("dnd")) return R.drawable.ic_qs_dnd_on; 441 else if (mSpec.equals("rotation")) return R.drawable.ic_portrait_from_auto_rotate; 442 else if (mSpec.equals("flashlight")) return R.drawable.ic_signal_flashlight_enable; 443 else if (mSpec.equals("location")) return R.drawable.ic_signal_location_enable; 444 else if (mSpec.equals("cast")) return R.drawable.ic_qs_cast_on; 445 else if (mSpec.equals("hotspot")) return R.drawable.ic_hotspot_enable; 446 return R.drawable.android; 447 } 448 449 @Override 450 public int getMetricsCategory() { 451 return 20000; 452 } 453 454 @Override 455 public boolean equals(Object o) { 456 if (o instanceof DraggableTile) { 457 return mSpec.equals(((DraggableTile) o).mSpec); 458 } 459 return false; 460 } 461 462 @Override 463 public void onDrop(String sourceText) { 464 ((CustomHost) mHost).replace(mSpec, sourceText); 465 } 466 467 } 468 469 private class DragHelper implements OnDragListener { 470 471 private final View mView; 472 private final DropListener mListener; 473 474 public DragHelper(View view, DropListener dropListener) { 475 mView = view; 476 mListener = dropListener; 477 mView.setOnDragListener(this); 478 } 479 480 @Override 481 public boolean onDrag(View v, DragEvent event) { 482 switch (event.getAction()) { 483 case DragEvent.ACTION_DRAG_ENTERED: 484 mView.setBackgroundColor(0x77ffffff); 485 break; 486 case DragEvent.ACTION_DRAG_ENDED: 487 stopDrag(); 488 case DragEvent.ACTION_DRAG_EXITED: 489 mView.setBackgroundColor(0x0); 490 break; 491 case DragEvent.ACTION_DROP: 492 stopDrag(); 493 String text = event.getClipData().getItemAt(0).getText().toString(); 494 mListener.onDrop(text); 495 break; 496 } 497 return true; 498 } 499 500 } 501 502 public interface DropListener { 503 void onDrop(String sourceText); 504 } 505 506 private class DraggableQsPanel extends QSPanel implements OnTouchListener { 507 public DraggableQsPanel(Context context) { 508 super(context); 509 mBrightnessView.setVisibility(View.GONE); 510 } 511 512 @Override 513 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 514 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 515 for (TileRecord r : mRecords) { 516 new DragHelper(r.tileView, (DraggableTile) r.tile); 517 r.tileView.setTag(r.tile); 518 r.tileView.setOnTouchListener(this); 519 520 for (int i = 0; i < r.tileView.getChildCount(); i++) { 521 r.tileView.getChildAt(i).setClickable(false); 522 } 523 } 524 } 525 526 @Override 527 public boolean onTouch(View v, MotionEvent event) { 528 switch (event.getAction()) { 529 case MotionEvent.ACTION_DOWN: 530 String tileSpec = (String) ((DraggableTile) v.getTag()).mSpec; 531 ClipData data = ClipData.newPlainText(tileSpec, tileSpec); 532 v.startDrag(data, new View.DragShadowBuilder(v), null, 0); 533 onStartDrag(); 534 return true; 535 } 536 return false; 537 } 538 } 539 540 } 541