Home | History | Annotate | Download | only in pm
      1 /*
      2  * Copyright (C) 2016 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.server.pm;
     17 
     18 import android.annotation.Nullable;
     19 import android.annotation.UserIdInt;
     20 import android.content.ComponentName;
     21 import android.content.Intent;
     22 import android.content.pm.ActivityInfo;
     23 import android.content.pm.ResolveInfo;
     24 import android.content.pm.ShortcutInfo;
     25 import android.content.res.TypedArray;
     26 import android.content.res.XmlResourceParser;
     27 import android.text.TextUtils;
     28 import android.util.ArraySet;
     29 import android.util.AttributeSet;
     30 import android.util.Log;
     31 import android.util.Slog;
     32 import android.util.TypedValue;
     33 import android.util.Xml;
     34 
     35 import com.android.internal.R;
     36 import com.android.internal.annotations.VisibleForTesting;
     37 
     38 import org.xmlpull.v1.XmlPullParser;
     39 import org.xmlpull.v1.XmlPullParserException;
     40 
     41 import java.io.IOException;
     42 import java.util.ArrayList;
     43 import java.util.List;
     44 import java.util.Set;
     45 
     46 public class ShortcutParser {
     47     private static final String TAG = ShortcutService.TAG;
     48 
     49     private static final boolean DEBUG = ShortcutService.DEBUG || false; // DO NOT SUBMIT WITH TRUE
     50 
     51     @VisibleForTesting
     52     static final String METADATA_KEY = "android.app.shortcuts";
     53 
     54     private static final String TAG_SHORTCUTS = "shortcuts";
     55     private static final String TAG_SHORTCUT = "shortcut";
     56     private static final String TAG_INTENT = "intent";
     57     private static final String TAG_CATEGORIES = "categories";
     58 
     59     @Nullable
     60     public static List<ShortcutInfo> parseShortcuts(ShortcutService service,
     61             String packageName, @UserIdInt int userId) throws IOException, XmlPullParserException {
     62         if (ShortcutService.DEBUG) {
     63             Slog.d(TAG, String.format("Scanning package %s for manifest shortcuts on user %d",
     64                     packageName, userId));
     65         }
     66         final List<ResolveInfo> activities = service.injectGetMainActivities(packageName, userId);
     67         if (activities == null || activities.size() == 0) {
     68             return null;
     69         }
     70 
     71         List<ShortcutInfo> result = null;
     72 
     73         try {
     74             final int size = activities.size();
     75             for (int i = 0; i < size; i++) {
     76                 final ActivityInfo activityInfoNoMetadata = activities.get(i).activityInfo;
     77                 if (activityInfoNoMetadata == null) {
     78                     continue;
     79                 }
     80 
     81                 final ActivityInfo activityInfoWithMetadata =
     82                         service.getActivityInfoWithMetadata(
     83                         activityInfoNoMetadata.getComponentName(), userId);
     84                 if (activityInfoWithMetadata != null) {
     85                     result = parseShortcutsOneFile(
     86                             service, activityInfoWithMetadata, packageName, userId, result);
     87                 }
     88             }
     89         } catch (RuntimeException e) {
     90             // Resource ID mismatch may cause various runtime exceptions when parsing XMLs,
     91             // But we don't crash the device, so just swallow them.
     92             service.wtf(
     93                     "Exception caught while parsing shortcut XML for package=" + packageName, e);
     94             return null;
     95         }
     96         return result;
     97     }
     98 
     99     private static List<ShortcutInfo> parseShortcutsOneFile(
    100             ShortcutService service,
    101             ActivityInfo activityInfo, String packageName, @UserIdInt int userId,
    102             List<ShortcutInfo> result) throws IOException, XmlPullParserException {
    103         if (ShortcutService.DEBUG) {
    104             Slog.d(TAG, String.format(
    105                     "Checking main activity %s", activityInfo.getComponentName()));
    106         }
    107 
    108         XmlResourceParser parser = null;
    109         try {
    110             parser = service.injectXmlMetaData(activityInfo, METADATA_KEY);
    111             if (parser == null) {
    112                 return result;
    113             }
    114 
    115             final ComponentName activity = new ComponentName(packageName, activityInfo.name);
    116 
    117             final AttributeSet attrs = Xml.asAttributeSet(parser);
    118 
    119             int type;
    120 
    121             int rank = 0;
    122             final int maxShortcuts = service.getMaxActivityShortcuts();
    123             int numShortcuts = 0;
    124 
    125             // We instantiate ShortcutInfo at <shortcut>, but we add it to the list at </shortcut>,
    126             // after parsing <intent>.  We keep the current one in here.
    127             ShortcutInfo currentShortcut = null;
    128 
    129             Set<String> categories = null;
    130             final ArrayList<Intent> intents = new ArrayList<>();
    131 
    132             outer:
    133             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
    134                     && (type != XmlPullParser.END_TAG || parser.getDepth() > 0)) {
    135                 final int depth = parser.getDepth();
    136                 final String tag = parser.getName();
    137 
    138                 // When a shortcut tag is closing, publish.
    139                 if ((type == XmlPullParser.END_TAG) && (depth == 2) && (TAG_SHORTCUT.equals(tag))) {
    140                     if (currentShortcut == null) {
    141                         // Shortcut was invalid.
    142                         continue;
    143                     }
    144                     final ShortcutInfo si = currentShortcut;
    145                     currentShortcut = null; // Make sure to null out for the next iteration.
    146 
    147                     if (si.isEnabled()) {
    148                         if (intents.size() == 0) {
    149                             Log.e(TAG, "Shortcut " + si.getId() + " has no intent. Skipping it.");
    150                             continue;
    151                         }
    152                     } else {
    153                         // Just set the default intent to disabled shortcuts.
    154                         intents.clear();
    155                         intents.add(new Intent(Intent.ACTION_VIEW));
    156                     }
    157 
    158                     if (numShortcuts >= maxShortcuts) {
    159                         Log.e(TAG, "More than " + maxShortcuts + " shortcuts found for "
    160                                 + activityInfo.getComponentName() + ". Skipping the rest.");
    161                         return result;
    162                     }
    163 
    164                     // Same flag as what TaskStackBuilder adds.
    165                     intents.get(0).addFlags(
    166                             Intent.FLAG_ACTIVITY_NEW_TASK |
    167                             Intent.FLAG_ACTIVITY_CLEAR_TASK |
    168                             Intent.FLAG_ACTIVITY_TASK_ON_HOME);
    169                     try {
    170                         si.setIntents(intents.toArray(new Intent[intents.size()]));
    171                     } catch (RuntimeException e) {
    172                         // This shouldn't happen because intents in XML can't have complicated
    173                         // extras, but just in case Intent.parseIntent() supports such a thing one
    174                         // day.
    175                         Log.e(TAG, "Shortcut's extras contain un-persistable values. Skipping it.");
    176                         continue;
    177                     }
    178                     intents.clear();
    179 
    180                     if (categories != null) {
    181                         si.setCategories(categories);
    182                         categories = null;
    183                     }
    184 
    185                     if (result == null) {
    186                         result = new ArrayList<>();
    187                     }
    188                     result.add(si);
    189                     numShortcuts++;
    190                     rank++;
    191                     if (ShortcutService.DEBUG) {
    192                         Slog.d(TAG, "Shortcut added: " + si.toInsecureString());
    193                     }
    194                     continue;
    195                 }
    196 
    197                 // Otherwise, just look at start tags.
    198                 if (type != XmlPullParser.START_TAG) {
    199                     continue;
    200                 }
    201 
    202                 if (depth == 1 && TAG_SHORTCUTS.equals(tag)) {
    203                     continue; // Root tag.
    204                 }
    205                 if (depth == 2 && TAG_SHORTCUT.equals(tag)) {
    206                     final ShortcutInfo si = parseShortcutAttributes(
    207                             service, attrs, packageName, activity, userId, rank);
    208                     if (si == null) {
    209                         // Shortcut was invalid.
    210                         continue;
    211                     }
    212                     if (ShortcutService.DEBUG) {
    213                         Slog.d(TAG, "Shortcut found: " + si.toInsecureString());
    214                     }
    215                     if (result != null) {
    216                         for (int i = result.size() - 1; i >= 0; i--) {
    217                             if (si.getId().equals(result.get(i).getId())) {
    218                                 Log.e(TAG, "Duplicate shortcut ID detected. Skipping it.");
    219                                 continue outer;
    220                             }
    221                         }
    222                     }
    223                     currentShortcut = si;
    224                     categories = null;
    225                     continue;
    226                 }
    227                 if (depth == 3 && TAG_INTENT.equals(tag)) {
    228                     if ((currentShortcut == null)
    229                             || !currentShortcut.isEnabled()) {
    230                         Log.e(TAG, "Ignoring excessive intent tag.");
    231                         continue;
    232                     }
    233 
    234                     final Intent intent = Intent.parseIntent(service.mContext.getResources(),
    235                             parser, attrs);
    236                     if (TextUtils.isEmpty(intent.getAction())) {
    237                         Log.e(TAG, "Shortcut intent action must be provided. activity=" + activity);
    238                         currentShortcut = null; // Invalidate the current shortcut.
    239                         continue;
    240                     }
    241                     intents.add(intent);
    242                     continue;
    243                 }
    244                 if (depth == 3 && TAG_CATEGORIES.equals(tag)) {
    245                     if ((currentShortcut == null)
    246                             || (currentShortcut.getCategories() != null)) {
    247                         continue;
    248                     }
    249                     final String name = parseCategories(service, attrs);
    250                     if (TextUtils.isEmpty(name)) {
    251                         Log.e(TAG, "Empty category found. activity=" + activity);
    252                         continue;
    253                     }
    254 
    255                     if (categories == null) {
    256                         categories = new ArraySet<>();
    257                     }
    258                     categories.add(name);
    259                     continue;
    260                 }
    261 
    262                 Log.w(TAG, String.format("Invalid tag '%s' found at depth %d", tag, depth));
    263             }
    264         } finally {
    265             if (parser != null) {
    266                 parser.close();
    267             }
    268         }
    269         return result;
    270     }
    271 
    272     private static String parseCategories(ShortcutService service, AttributeSet attrs) {
    273         final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs,
    274                 R.styleable.ShortcutCategories);
    275         try {
    276             if (sa.getType(R.styleable.ShortcutCategories_name) == TypedValue.TYPE_STRING) {
    277                 return sa.getNonResourceString(R.styleable.ShortcutCategories_name);
    278             } else {
    279                 Log.w(TAG, "android:name for shortcut category must be string literal.");
    280                 return null;
    281             }
    282         } finally {
    283             sa.recycle();
    284         }
    285     }
    286 
    287     private static ShortcutInfo parseShortcutAttributes(ShortcutService service,
    288             AttributeSet attrs, String packageName, ComponentName activity,
    289             @UserIdInt int userId, int rank) {
    290         final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs,
    291                 R.styleable.Shortcut);
    292         try {
    293             if (sa.getType(R.styleable.Shortcut_shortcutId) != TypedValue.TYPE_STRING) {
    294                 Log.w(TAG, "android:shortcutId must be string literal. activity=" + activity);
    295                 return null;
    296             }
    297             final String id = sa.getNonResourceString(R.styleable.Shortcut_shortcutId);
    298             final boolean enabled = sa.getBoolean(R.styleable.Shortcut_enabled, true);
    299             final int iconResId = sa.getResourceId(R.styleable.Shortcut_icon, 0);
    300             final int titleResId = sa.getResourceId(R.styleable.Shortcut_shortcutShortLabel, 0);
    301             final int textResId = sa.getResourceId(R.styleable.Shortcut_shortcutLongLabel, 0);
    302             final int disabledMessageResId = sa.getResourceId(
    303                     R.styleable.Shortcut_shortcutDisabledMessage, 0);
    304 
    305             if (TextUtils.isEmpty(id)) {
    306                 Log.w(TAG, "android:shortcutId must be provided. activity=" + activity);
    307                 return null;
    308             }
    309             if (titleResId == 0) {
    310                 Log.w(TAG, "android:shortcutShortLabel must be provided. activity=" + activity);
    311                 return null;
    312             }
    313 
    314             return createShortcutFromManifest(
    315                     service,
    316                     userId,
    317                     id,
    318                     packageName,
    319                     activity,
    320                     titleResId,
    321                     textResId,
    322                     disabledMessageResId,
    323                     rank,
    324                     iconResId,
    325                     enabled);
    326         } finally {
    327             sa.recycle();
    328         }
    329     }
    330 
    331     private static ShortcutInfo createShortcutFromManifest(ShortcutService service,
    332             @UserIdInt int userId, String id, String packageName, ComponentName activityComponent,
    333             int titleResId, int textResId, int disabledMessageResId,
    334             int rank, int iconResId, boolean enabled) {
    335 
    336         final int flags =
    337                 (enabled ? ShortcutInfo.FLAG_MANIFEST : ShortcutInfo.FLAG_DISABLED)
    338                 | ShortcutInfo.FLAG_IMMUTABLE
    339                 | ((iconResId != 0) ? ShortcutInfo.FLAG_HAS_ICON_RES : 0);
    340         final int disabledReason =
    341                 enabled ? ShortcutInfo.DISABLED_REASON_NOT_DISABLED
    342                         : ShortcutInfo.DISABLED_REASON_BY_APP;
    343 
    344         // Note we don't need to set resource names here yet.  They'll be set when they're about
    345         // to be published.
    346         return new ShortcutInfo(
    347                 userId,
    348                 id,
    349                 packageName,
    350                 activityComponent,
    351                 null, // icon
    352                 null, // title string
    353                 titleResId,
    354                 null, // title res name
    355                 null, // text string
    356                 textResId,
    357                 null, // text res name
    358                 null, // disabled message string
    359                 disabledMessageResId,
    360                 null, // disabled message res name
    361                 null, // categories
    362                 null, // intent
    363                 rank,
    364                 null, // extras
    365                 service.injectCurrentTimeMillis(),
    366                 flags,
    367                 iconResId,
    368                 null, // icon res name
    369                 null, // bitmap path
    370                 disabledReason);
    371     }
    372 }
    373