Home | History | Annotate | Download | only in tuner
      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