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