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