Home | History | Annotate | Download | only in external
      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.qs.external;
     17 
     18 import android.app.ActivityManager;
     19 import android.content.ComponentName;
     20 import android.content.Intent;
     21 import android.content.pm.PackageManager;
     22 import android.content.pm.ResolveInfo;
     23 import android.content.pm.ServiceInfo;
     24 import android.graphics.drawable.Drawable;
     25 import android.net.Uri;
     26 import android.os.Binder;
     27 import android.os.IBinder;
     28 import android.os.RemoteException;
     29 import android.provider.Settings;
     30 import android.service.quicksettings.IQSTileService;
     31 import android.service.quicksettings.Tile;
     32 import android.service.quicksettings.TileService;
     33 import android.text.SpannableStringBuilder;
     34 import android.text.style.ForegroundColorSpan;
     35 import android.util.Log;
     36 import android.view.IWindowManager;
     37 import android.view.WindowManager;
     38 import android.view.WindowManagerGlobal;
     39 import com.android.internal.logging.MetricsLogger;
     40 import com.android.internal.logging.MetricsProto.MetricsEvent;
     41 import com.android.systemui.R;
     42 import com.android.systemui.qs.QSTile;
     43 import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener;
     44 import com.android.systemui.statusbar.phone.QSTileHost;
     45 import libcore.util.Objects;
     46 
     47 public class CustomTile extends QSTile<QSTile.State> implements TileChangeListener {
     48     public static final String PREFIX = "custom(";
     49 
     50     private static final boolean DEBUG = false;
     51 
     52     // We don't want to thrash binding and unbinding if the user opens and closes the panel a lot.
     53     // So instead we have a period of waiting.
     54     private static final long UNBIND_DELAY = 30000;
     55 
     56     private final ComponentName mComponent;
     57     private final Tile mTile;
     58     private final IWindowManager mWindowManager;
     59     private final IBinder mToken = new Binder();
     60     private final IQSTileService mService;
     61     private final TileServiceManager mServiceManager;
     62     private final int mUser;
     63     private android.graphics.drawable.Icon mDefaultIcon;
     64 
     65     private boolean mListening;
     66     private boolean mBound;
     67     private boolean mIsTokenGranted;
     68     private boolean mIsShowingDialog;
     69 
     70     private CustomTile(QSTileHost host, String action) {
     71         super(host);
     72         mWindowManager = WindowManagerGlobal.getWindowManagerService();
     73         mComponent = ComponentName.unflattenFromString(action);
     74         mTile = new Tile();
     75         setTileIcon();
     76         mServiceManager = host.getTileServices().getTileWrapper(this);
     77         mService = mServiceManager.getTileService();
     78         mServiceManager.setTileChangeListener(this);
     79         mUser = ActivityManager.getCurrentUser();
     80     }
     81 
     82     private void setTileIcon() {
     83         try {
     84             PackageManager pm = mContext.getPackageManager();
     85             int flags = PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE;
     86             if (isSystemApp(pm)) {
     87                 flags |= PackageManager.MATCH_DISABLED_COMPONENTS;
     88             }
     89             ServiceInfo info = pm.getServiceInfo(mComponent, flags);
     90             int icon = info.icon != 0 ? info.icon
     91                     : info.applicationInfo.icon;
     92             // Update the icon if its not set or is the default icon.
     93             boolean updateIcon = mTile.getIcon() == null
     94                     || iconEquals(mTile.getIcon(), mDefaultIcon);
     95             mDefaultIcon = icon != 0 ? android.graphics.drawable.Icon
     96                     .createWithResource(mComponent.getPackageName(), icon) : null;
     97             if (updateIcon) {
     98                 mTile.setIcon(mDefaultIcon);
     99             }
    100             // Update the label if there is no label.
    101             if (mTile.getLabel() == null) {
    102                 mTile.setLabel(info.loadLabel(pm));
    103             }
    104         } catch (Exception e) {
    105             mDefaultIcon = null;
    106         }
    107     }
    108 
    109     private boolean isSystemApp(PackageManager pm) throws PackageManager.NameNotFoundException {
    110         return pm.getApplicationInfo(mComponent.getPackageName(), 0).isSystemApp();
    111     }
    112 
    113     /**
    114      * Compare two icons, only works for resources.
    115      */
    116     private boolean iconEquals(android.graphics.drawable.Icon icon1,
    117             android.graphics.drawable.Icon icon2) {
    118         if (icon1 == icon2) {
    119             return true;
    120         }
    121         if (icon1 == null || icon2 == null) {
    122             return false;
    123         }
    124         if (icon1.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE
    125                 || icon2.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE) {
    126             return false;
    127         }
    128         if (icon1.getResId() != icon2.getResId()) {
    129             return false;
    130         }
    131         if (!Objects.equal(icon1.getResPackage(), icon2.getResPackage())) {
    132             return false;
    133         }
    134         return true;
    135     }
    136 
    137     @Override
    138     public void onTileChanged(ComponentName tile) {
    139         setTileIcon();
    140     }
    141 
    142     @Override
    143     public boolean isAvailable() {
    144         return mDefaultIcon != null;
    145     }
    146 
    147     public int getUser() {
    148         return mUser;
    149     }
    150 
    151     public ComponentName getComponent() {
    152         return mComponent;
    153     }
    154 
    155     public Tile getQsTile() {
    156         return mTile;
    157     }
    158 
    159     public void updateState(Tile tile) {
    160         mTile.setIcon(tile.getIcon());
    161         mTile.setLabel(tile.getLabel());
    162         mTile.setContentDescription(tile.getContentDescription());
    163         mTile.setState(tile.getState());
    164     }
    165 
    166     public void onDialogShown() {
    167         mIsShowingDialog = true;
    168     }
    169 
    170     public void onDialogHidden() {
    171         mIsShowingDialog = false;
    172         try {
    173             if (DEBUG) Log.d(TAG, "Removing token");
    174             mWindowManager.removeWindowToken(mToken);
    175         } catch (RemoteException e) {
    176         }
    177     }
    178 
    179     @Override
    180     public void setListening(boolean listening) {
    181         if (mListening == listening) return;
    182         mListening = listening;
    183         try {
    184             if (listening) {
    185                 setTileIcon();
    186                 refreshState();
    187                 if (!mServiceManager.isActiveTile()) {
    188                     mServiceManager.setBindRequested(true);
    189                     mService.onStartListening();
    190                 }
    191             } else {
    192                 mService.onStopListening();
    193                 if (mIsTokenGranted && !mIsShowingDialog) {
    194                     try {
    195                         if (DEBUG) Log.d(TAG, "Removing token");
    196                         mWindowManager.removeWindowToken(mToken);
    197                     } catch (RemoteException e) {
    198                     }
    199                     mIsTokenGranted = false;
    200                 }
    201                 mIsShowingDialog = false;
    202                 mServiceManager.setBindRequested(false);
    203             }
    204         } catch (RemoteException e) {
    205             // Called through wrapper, won't happen here.
    206         }
    207     }
    208 
    209     @Override
    210     protected void handleDestroy() {
    211         super.handleDestroy();
    212         if (mIsTokenGranted) {
    213             try {
    214                 if (DEBUG) Log.d(TAG, "Removing token");
    215                 mWindowManager.removeWindowToken(mToken);
    216             } catch (RemoteException e) {
    217             }
    218         }
    219         mHost.getTileServices().freeService(this, mServiceManager);
    220     }
    221 
    222     @Override
    223     public State newTileState() {
    224         return new State();
    225     }
    226 
    227     @Override
    228     public Intent getLongClickIntent() {
    229         Intent i = new Intent(TileService.ACTION_QS_TILE_PREFERENCES);
    230         i.setPackage(mComponent.getPackageName());
    231         i = resolveIntent(i);
    232         if (i != null) {
    233             return i;
    234         }
    235         return new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
    236                 Uri.fromParts("package", mComponent.getPackageName(), null));
    237     }
    238 
    239     private Intent resolveIntent(Intent i) {
    240         ResolveInfo result = mContext.getPackageManager().resolveActivityAsUser(i, 0,
    241                 ActivityManager.getCurrentUser());
    242         return result != null ? new Intent(TileService.ACTION_QS_TILE_PREFERENCES)
    243                 .setClassName(result.activityInfo.packageName, result.activityInfo.name) : null;
    244     }
    245 
    246     @Override
    247     protected void handleClick() {
    248         if (mTile.getState() == Tile.STATE_UNAVAILABLE) {
    249             return;
    250         }
    251         try {
    252             if (DEBUG) Log.d(TAG, "Adding token");
    253             mWindowManager.addWindowToken(mToken, WindowManager.LayoutParams.TYPE_QS_DIALOG);
    254             mIsTokenGranted = true;
    255         } catch (RemoteException e) {
    256         }
    257         try {
    258             if (mServiceManager.isActiveTile()) {
    259                 mServiceManager.setBindRequested(true);
    260                 mService.onStartListening();
    261             }
    262             mService.onClick(mToken);
    263         } catch (RemoteException e) {
    264             // Called through wrapper, won't happen here.
    265         }
    266         MetricsLogger.action(mContext, getMetricsCategory(), mComponent.getPackageName());
    267     }
    268 
    269     @Override
    270     public CharSequence getTileLabel() {
    271         return getState().label;
    272     }
    273 
    274     @Override
    275     protected void handleUpdateState(State state, Object arg) {
    276         int tileState = mTile.getState();
    277         if (mServiceManager.hasPendingBind()) {
    278             tileState = Tile.STATE_UNAVAILABLE;
    279         }
    280         Drawable drawable;
    281         try {
    282             drawable = mTile.getIcon().loadDrawable(mContext);
    283         } catch (Exception e) {
    284             Log.w(TAG, "Invalid icon, forcing into unavailable state");
    285             tileState = Tile.STATE_UNAVAILABLE;
    286             drawable = mDefaultIcon.loadDrawable(mContext);
    287         }
    288         int color = mContext.getColor(getColor(tileState));
    289         drawable.setTint(color);
    290         state.icon = new DrawableIcon(drawable);
    291         state.label = mTile.getLabel();
    292         if (tileState == Tile.STATE_UNAVAILABLE) {
    293             state.label = new SpannableStringBuilder().append(state.label,
    294                     new ForegroundColorSpan(color),
    295                     SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
    296         }
    297         if (mTile.getContentDescription() != null) {
    298             state.contentDescription = mTile.getContentDescription();
    299         } else {
    300             state.contentDescription = state.label;
    301         }
    302     }
    303 
    304     @Override
    305     public int getMetricsCategory() {
    306         return MetricsEvent.QS_CUSTOM;
    307     }
    308 
    309     public void startUnlockAndRun() {
    310         mHost.startRunnableDismissingKeyguard(new Runnable() {
    311             @Override
    312             public void run() {
    313                 try {
    314                     mService.onUnlockComplete();
    315                 } catch (RemoteException e) {
    316                 }
    317             }
    318         });
    319     }
    320 
    321     private static int getColor(int state) {
    322         switch (state) {
    323             case Tile.STATE_UNAVAILABLE:
    324                 return R.color.qs_tile_tint_unavailable;
    325             case Tile.STATE_INACTIVE:
    326                 return R.color.qs_tile_tint_inactive;
    327             case Tile.STATE_ACTIVE:
    328                 return R.color.qs_tile_tint_active;
    329         }
    330         return 0;
    331     }
    332 
    333     public static String toSpec(ComponentName name) {
    334         return PREFIX + name.flattenToShortString() + ")";
    335     }
    336 
    337     public static ComponentName getComponentFromSpec(String spec) {
    338         final String action = spec.substring(PREFIX.length(), spec.length() - 1);
    339         if (action.isEmpty()) {
    340             throw new IllegalArgumentException("Empty custom tile spec action");
    341         }
    342         return ComponentName.unflattenFromString(action);
    343     }
    344 
    345     public static QSTile<?> create(QSTileHost host, String spec) {
    346         if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) {
    347             throw new IllegalArgumentException("Bad custom tile spec: " + spec);
    348         }
    349         final String action = spec.substring(PREFIX.length(), spec.length() - 1);
    350         if (action.isEmpty()) {
    351             throw new IllegalArgumentException("Empty custom tile spec action");
    352         }
    353         return new CustomTile(host, action);
    354     }
    355 }
    356