Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 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 | |
Mehdi Alizadeh | 3277462 | 2018-11-05 17:32:01 -0800 | [diff] [blame] | 18 | import android.annotation.NonNull; |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 19 | import android.annotation.Nullable; |
| 20 | import android.annotation.UserIdInt; |
| 21 | import android.content.ComponentName; |
| 22 | import android.content.Intent; |
| 23 | import android.content.pm.ActivityInfo; |
Makoto Onuki | b08790c | 2016-06-23 14:05:46 -0700 | [diff] [blame] | 24 | import android.content.pm.ResolveInfo; |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 25 | import android.content.pm.ShortcutInfo; |
| 26 | import android.content.res.TypedArray; |
| 27 | import android.content.res.XmlResourceParser; |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 28 | import android.text.TextUtils; |
| 29 | import android.util.ArraySet; |
| 30 | import android.util.AttributeSet; |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 31 | import android.util.Log; |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 32 | import android.util.Slog; |
Makoto Onuki | 6771d73 | 2016-07-14 12:58:40 -0700 | [diff] [blame] | 33 | import android.util.TypedValue; |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 34 | import android.util.Xml; |
| 35 | |
| 36 | import com.android.internal.R; |
| 37 | import com.android.internal.annotations.VisibleForTesting; |
| 38 | |
| 39 | import org.xmlpull.v1.XmlPullParser; |
| 40 | import org.xmlpull.v1.XmlPullParserException; |
| 41 | |
| 42 | import java.io.IOException; |
| 43 | import java.util.ArrayList; |
| 44 | import java.util.List; |
| 45 | import java.util.Set; |
| 46 | |
| 47 | public class ShortcutParser { |
| 48 | private static final String TAG = ShortcutService.TAG; |
| 49 | |
| 50 | private static final boolean DEBUG = ShortcutService.DEBUG || false; // DO NOT SUBMIT WITH TRUE |
| 51 | |
| 52 | @VisibleForTesting |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 53 | static final String METADATA_KEY = "android.app.shortcuts"; |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 54 | |
| 55 | private static final String TAG_SHORTCUTS = "shortcuts"; |
| 56 | private static final String TAG_SHORTCUT = "shortcut"; |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 57 | private static final String TAG_INTENT = "intent"; |
| 58 | private static final String TAG_CATEGORIES = "categories"; |
Mehdi Alizadeh | 3277462 | 2018-11-05 17:32:01 -0800 | [diff] [blame] | 59 | private static final String TAG_SHARE_TARGET = "share-target"; |
| 60 | private static final String TAG_DATA = "data"; |
| 61 | private static final String TAG_CATEGORY = "category"; |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 62 | |
| 63 | @Nullable |
Mehdi Alizadeh | 3277462 | 2018-11-05 17:32:01 -0800 | [diff] [blame] | 64 | public static List<ShortcutInfo> parseShortcuts(ShortcutService service, String packageName, |
| 65 | @UserIdInt int userId, @NonNull List<ShareTargetInfo> outShareTargets) |
| 66 | throws IOException, XmlPullParserException { |
Makoto Onuki | b08790c | 2016-06-23 14:05:46 -0700 | [diff] [blame] | 67 | if (ShortcutService.DEBUG) { |
| 68 | Slog.d(TAG, String.format("Scanning package %s for manifest shortcuts on user %d", |
| 69 | packageName, userId)); |
| 70 | } |
| 71 | final List<ResolveInfo> activities = service.injectGetMainActivities(packageName, userId); |
| 72 | if (activities == null || activities.size() == 0) { |
| 73 | return null; |
| 74 | } |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 75 | |
| 76 | List<ShortcutInfo> result = null; |
Mehdi Alizadeh | 3277462 | 2018-11-05 17:32:01 -0800 | [diff] [blame] | 77 | outShareTargets.clear(); |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 78 | |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 79 | try { |
Makoto Onuki | b08790c | 2016-06-23 14:05:46 -0700 | [diff] [blame] | 80 | final int size = activities.size(); |
| 81 | for (int i = 0; i < size; i++) { |
| 82 | final ActivityInfo activityInfoNoMetadata = activities.get(i).activityInfo; |
| 83 | if (activityInfoNoMetadata == null) { |
| 84 | continue; |
| 85 | } |
| 86 | |
| 87 | final ActivityInfo activityInfoWithMetadata = |
Makoto Onuki | ee6b6e4 | 2016-06-29 17:34:02 -0700 | [diff] [blame] | 88 | service.getActivityInfoWithMetadata( |
Makoto Onuki | b08790c | 2016-06-23 14:05:46 -0700 | [diff] [blame] | 89 | activityInfoNoMetadata.getComponentName(), userId); |
| 90 | if (activityInfoWithMetadata != null) { |
Mehdi Alizadeh | 3277462 | 2018-11-05 17:32:01 -0800 | [diff] [blame] | 91 | result = parseShortcutsOneFile(service, activityInfoWithMetadata, packageName, |
| 92 | userId, result, outShareTargets); |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 93 | } |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 94 | } |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 95 | } catch (RuntimeException e) { |
Makoto Onuki | b08790c | 2016-06-23 14:05:46 -0700 | [diff] [blame] | 96 | // Resource ID mismatch may cause various runtime exceptions when parsing XMLs, |
| 97 | // But we don't crash the device, so just swallow them. |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 98 | service.wtf( |
| 99 | "Exception caught while parsing shortcut XML for package=" + packageName, e); |
| 100 | return null; |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 101 | } |
| 102 | return result; |
| 103 | } |
| 104 | |
| 105 | private static List<ShortcutInfo> parseShortcutsOneFile( |
| 106 | ShortcutService service, |
| 107 | ActivityInfo activityInfo, String packageName, @UserIdInt int userId, |
Mehdi Alizadeh | 3277462 | 2018-11-05 17:32:01 -0800 | [diff] [blame] | 108 | List<ShortcutInfo> result, @NonNull List<ShareTargetInfo> outShareTargets) |
| 109 | throws IOException, XmlPullParserException { |
Makoto Onuki | b08790c | 2016-06-23 14:05:46 -0700 | [diff] [blame] | 110 | if (ShortcutService.DEBUG) { |
| 111 | Slog.d(TAG, String.format( |
| 112 | "Checking main activity %s", activityInfo.getComponentName())); |
| 113 | } |
| 114 | |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 115 | XmlResourceParser parser = null; |
| 116 | try { |
| 117 | parser = service.injectXmlMetaData(activityInfo, METADATA_KEY); |
| 118 | if (parser == null) { |
| 119 | return result; |
| 120 | } |
| 121 | |
| 122 | final ComponentName activity = new ComponentName(packageName, activityInfo.name); |
| 123 | |
| 124 | final AttributeSet attrs = Xml.asAttributeSet(parser); |
| 125 | |
| 126 | int type; |
| 127 | |
Makoto Onuki | eddbfec | 2016-05-31 17:04:34 -0700 | [diff] [blame] | 128 | int rank = 0; |
Makoto Onuki | 7001a61 | 2016-05-27 13:24:28 -0700 | [diff] [blame] | 129 | final int maxShortcuts = service.getMaxActivityShortcuts(); |
| 130 | int numShortcuts = 0; |
Makoto Onuki | eddbfec | 2016-05-31 17:04:34 -0700 | [diff] [blame] | 131 | |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 132 | // We instantiate ShortcutInfo at <shortcut>, but we add it to the list at </shortcut>, |
| 133 | // after parsing <intent>. We keep the current one in here. |
| 134 | ShortcutInfo currentShortcut = null; |
| 135 | |
Mehdi Alizadeh | 3277462 | 2018-11-05 17:32:01 -0800 | [diff] [blame] | 136 | // We instantiate ShareTargetInfo at <share-target>, but add it to outShareTargets at |
| 137 | // </share-target>, after parsing <data> and <category>. We keep the current one here. |
| 138 | ShareTargetInfo currentShareTarget = null; |
| 139 | |
| 140 | // Keeps parsed categories for both ShortcutInfo and ShareTargetInfo |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 141 | Set<String> categories = null; |
Mehdi Alizadeh | 3277462 | 2018-11-05 17:32:01 -0800 | [diff] [blame] | 142 | |
| 143 | // Keeps parsed intents for ShortcutInfo |
Makoto Onuki | 440a1ea | 2016-07-20 14:21:18 -0700 | [diff] [blame] | 144 | final ArrayList<Intent> intents = new ArrayList<>(); |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 145 | |
Mehdi Alizadeh | 3277462 | 2018-11-05 17:32:01 -0800 | [diff] [blame] | 146 | // Keeps parsed data fields for ShareTargetInfo |
| 147 | final ArrayList<ShareTargetInfo.TargetData> dataList = new ArrayList<>(); |
| 148 | |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 149 | outer: |
| 150 | while ((type = parser.next()) != XmlPullParser.END_DOCUMENT |
| 151 | && (type != XmlPullParser.END_TAG || parser.getDepth() > 0)) { |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 152 | final int depth = parser.getDepth(); |
| 153 | final String tag = parser.getName(); |
| 154 | |
| 155 | // When a shortcut tag is closing, publish. |
| 156 | if ((type == XmlPullParser.END_TAG) && (depth == 2) && (TAG_SHORTCUT.equals(tag))) { |
| 157 | if (currentShortcut == null) { |
| 158 | // Shortcut was invalid. |
| 159 | continue; |
| 160 | } |
| 161 | final ShortcutInfo si = currentShortcut; |
| 162 | currentShortcut = null; // Make sure to null out for the next iteration. |
| 163 | |
Makoto Onuki | 440a1ea | 2016-07-20 14:21:18 -0700 | [diff] [blame] | 164 | if (si.isEnabled()) { |
| 165 | if (intents.size() == 0) { |
| 166 | Log.e(TAG, "Shortcut " + si.getId() + " has no intent. Skipping it."); |
| 167 | continue; |
| 168 | } |
| 169 | } else { |
| 170 | // Just set the default intent to disabled shortcuts. |
| 171 | intents.clear(); |
| 172 | intents.add(new Intent(Intent.ACTION_VIEW)); |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 173 | } |
| 174 | |
| 175 | if (numShortcuts >= maxShortcuts) { |
| 176 | Log.e(TAG, "More than " + maxShortcuts + " shortcuts found for " |
| 177 | + activityInfo.getComponentName() + ". Skipping the rest."); |
| 178 | return result; |
| 179 | } |
Makoto Onuki | 440a1ea | 2016-07-20 14:21:18 -0700 | [diff] [blame] | 180 | |
| 181 | // Same flag as what TaskStackBuilder adds. |
| 182 | intents.get(0).addFlags( |
| 183 | Intent.FLAG_ACTIVITY_NEW_TASK | |
| 184 | Intent.FLAG_ACTIVITY_CLEAR_TASK | |
| 185 | Intent.FLAG_ACTIVITY_TASK_ON_HOME); |
| 186 | try { |
| 187 | si.setIntents(intents.toArray(new Intent[intents.size()])); |
| 188 | } catch (RuntimeException e) { |
| 189 | // This shouldn't happen because intents in XML can't have complicated |
| 190 | // extras, but just in case Intent.parseIntent() supports such a thing one |
| 191 | // day. |
| 192 | Log.e(TAG, "Shortcut's extras contain un-persistable values. Skipping it."); |
| 193 | continue; |
| 194 | } |
| 195 | intents.clear(); |
| 196 | |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 197 | if (categories != null) { |
| 198 | si.setCategories(categories); |
| 199 | categories = null; |
| 200 | } |
| 201 | |
| 202 | if (result == null) { |
| 203 | result = new ArrayList<>(); |
| 204 | } |
| 205 | result.add(si); |
| 206 | numShortcuts++; |
| 207 | rank++; |
| 208 | if (ShortcutService.DEBUG) { |
| 209 | Slog.d(TAG, "Shortcut added: " + si.toInsecureString()); |
| 210 | } |
| 211 | continue; |
| 212 | } |
| 213 | |
Mehdi Alizadeh | 3277462 | 2018-11-05 17:32:01 -0800 | [diff] [blame] | 214 | // When a share-target tag is closing, publish. |
| 215 | if ((type == XmlPullParser.END_TAG) && (depth == 2) |
| 216 | && (TAG_SHARE_TARGET.equals(tag))) { |
| 217 | if (currentShareTarget == null) { |
| 218 | // ShareTarget was invalid. |
| 219 | continue; |
| 220 | } |
| 221 | final ShareTargetInfo sti = currentShareTarget; |
| 222 | currentShareTarget = null; // Make sure to null out for the next iteration. |
| 223 | |
| 224 | if (categories == null || categories.isEmpty() || dataList.isEmpty()) { |
| 225 | // Incomplete ShareTargetInfo. |
| 226 | continue; |
| 227 | } |
| 228 | |
| 229 | final ShareTargetInfo newShareTarget = new ShareTargetInfo( |
| 230 | dataList.toArray(new ShareTargetInfo.TargetData[dataList.size()]), |
| 231 | sti.mTargetClass, categories.toArray(new String[categories.size()])); |
| 232 | outShareTargets.add(newShareTarget); |
| 233 | if (ShortcutService.DEBUG) { |
| 234 | Slog.d(TAG, "ShareTarget added: " + newShareTarget.toString()); |
| 235 | } |
| 236 | categories = null; |
| 237 | dataList.clear(); |
| 238 | } |
| 239 | |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 240 | // Otherwise, just look at start tags. |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 241 | if (type != XmlPullParser.START_TAG) { |
| 242 | continue; |
| 243 | } |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 244 | |
| 245 | if (depth == 1 && TAG_SHORTCUTS.equals(tag)) { |
| 246 | continue; // Root tag. |
| 247 | } |
| 248 | if (depth == 2 && TAG_SHORTCUT.equals(tag)) { |
| 249 | final ShortcutInfo si = parseShortcutAttributes( |
Makoto Onuki | 9e1f559 | 2016-06-08 12:30:23 -0700 | [diff] [blame] | 250 | service, attrs, packageName, activity, userId, rank); |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 251 | if (si == null) { |
| 252 | // Shortcut was invalid. |
| 253 | continue; |
| 254 | } |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 255 | if (ShortcutService.DEBUG) { |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 256 | Slog.d(TAG, "Shortcut found: " + si.toInsecureString()); |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 257 | } |
| 258 | if (result != null) { |
| 259 | for (int i = result.size() - 1; i >= 0; i--) { |
| 260 | if (si.getId().equals(result.get(i).getId())) { |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 261 | Log.e(TAG, "Duplicate shortcut ID detected. Skipping it."); |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 262 | continue outer; |
| 263 | } |
| 264 | } |
| 265 | } |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 266 | currentShortcut = si; |
| 267 | categories = null; |
| 268 | continue; |
| 269 | } |
Mehdi Alizadeh | 3277462 | 2018-11-05 17:32:01 -0800 | [diff] [blame] | 270 | if (depth == 2 && TAG_SHARE_TARGET.equals(tag)) { |
| 271 | final ShareTargetInfo sti = parseShareTargetAttributes(service, attrs); |
| 272 | if (sti == null) { |
| 273 | // ShareTarget was invalid. |
| 274 | continue; |
| 275 | } |
| 276 | currentShareTarget = sti; |
| 277 | categories = null; |
| 278 | dataList.clear(); |
| 279 | continue; |
| 280 | } |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 281 | if (depth == 3 && TAG_INTENT.equals(tag)) { |
| 282 | if ((currentShortcut == null) |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 283 | || !currentShortcut.isEnabled()) { |
| 284 | Log.e(TAG, "Ignoring excessive intent tag."); |
| 285 | continue; |
| 286 | } |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 287 | |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 288 | final Intent intent = Intent.parseIntent(service.mContext.getResources(), |
| 289 | parser, attrs); |
| 290 | if (TextUtils.isEmpty(intent.getAction())) { |
| 291 | Log.e(TAG, "Shortcut intent action must be provided. activity=" + activity); |
Makoto Onuki | 440a1ea | 2016-07-20 14:21:18 -0700 | [diff] [blame] | 292 | currentShortcut = null; // Invalidate the current shortcut. |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 293 | continue; |
| 294 | } |
Makoto Onuki | 440a1ea | 2016-07-20 14:21:18 -0700 | [diff] [blame] | 295 | intents.add(intent); |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 296 | continue; |
| 297 | } |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 298 | if (depth == 3 && TAG_CATEGORIES.equals(tag)) { |
| 299 | if ((currentShortcut == null) |
| 300 | || (currentShortcut.getCategories() != null)) { |
| 301 | continue; |
| 302 | } |
| 303 | final String name = parseCategories(service, attrs); |
| 304 | if (TextUtils.isEmpty(name)) { |
| 305 | Log.e(TAG, "Empty category found. activity=" + activity); |
| 306 | continue; |
| 307 | } |
| 308 | |
| 309 | if (categories == null) { |
| 310 | categories = new ArraySet<>(); |
| 311 | } |
| 312 | categories.add(name); |
| 313 | continue; |
| 314 | } |
Mehdi Alizadeh | 3277462 | 2018-11-05 17:32:01 -0800 | [diff] [blame] | 315 | if (depth == 3 && TAG_CATEGORY.equals(tag)) { |
| 316 | if ((currentShareTarget == null)) { |
| 317 | continue; |
| 318 | } |
| 319 | final String name = parseCategory(service, attrs); |
| 320 | if (TextUtils.isEmpty(name)) { |
| 321 | Log.e(TAG, "Empty category found. activity=" + activity); |
| 322 | continue; |
| 323 | } |
| 324 | |
| 325 | if (categories == null) { |
| 326 | categories = new ArraySet<>(); |
| 327 | } |
| 328 | categories.add(name); |
| 329 | continue; |
| 330 | } |
| 331 | if (depth == 3 && TAG_DATA.equals(tag)) { |
| 332 | if ((currentShareTarget == null)) { |
| 333 | continue; |
| 334 | } |
| 335 | final ShareTargetInfo.TargetData data = parseShareTargetData(service, attrs); |
| 336 | if (data == null) { |
| 337 | Log.e(TAG, "Invalid data tag found. activity=" + activity); |
| 338 | continue; |
| 339 | } |
| 340 | dataList.add(data); |
| 341 | continue; |
| 342 | } |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 343 | |
Makoto Onuki | 9c85001 | 2016-07-26 15:50:50 -0700 | [diff] [blame] | 344 | Log.w(TAG, String.format("Invalid tag '%s' found at depth %d", tag, depth)); |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 345 | } |
| 346 | } finally { |
| 347 | if (parser != null) { |
| 348 | parser.close(); |
| 349 | } |
| 350 | } |
| 351 | return result; |
| 352 | } |
| 353 | |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 354 | private static String parseCategories(ShortcutService service, AttributeSet attrs) { |
| 355 | final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, |
| 356 | R.styleable.ShortcutCategories); |
| 357 | try { |
Makoto Onuki | 6771d73 | 2016-07-14 12:58:40 -0700 | [diff] [blame] | 358 | if (sa.getType(R.styleable.ShortcutCategories_name) == TypedValue.TYPE_STRING) { |
| 359 | return sa.getNonResourceString(R.styleable.ShortcutCategories_name); |
| 360 | } else { |
| 361 | Log.w(TAG, "android:name for shortcut category must be string literal."); |
| 362 | return null; |
| 363 | } |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 364 | } finally { |
| 365 | sa.recycle(); |
| 366 | } |
| 367 | } |
| 368 | |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 369 | private static ShortcutInfo parseShortcutAttributes(ShortcutService service, |
| 370 | AttributeSet attrs, String packageName, ComponentName activity, |
Makoto Onuki | eddbfec | 2016-05-31 17:04:34 -0700 | [diff] [blame] | 371 | @UserIdInt int userId, int rank) { |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 372 | final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, |
| 373 | R.styleable.Shortcut); |
| 374 | try { |
Makoto Onuki | 6771d73 | 2016-07-14 12:58:40 -0700 | [diff] [blame] | 375 | if (sa.getType(R.styleable.Shortcut_shortcutId) != TypedValue.TYPE_STRING) { |
| 376 | Log.w(TAG, "android:shortcutId must be string literal. activity=" + activity); |
| 377 | return null; |
| 378 | } |
| 379 | final String id = sa.getNonResourceString(R.styleable.Shortcut_shortcutId); |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 380 | final boolean enabled = sa.getBoolean(R.styleable.Shortcut_enabled, true); |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 381 | final int iconResId = sa.getResourceId(R.styleable.Shortcut_icon, 0); |
Makoto Onuki | eddbfec | 2016-05-31 17:04:34 -0700 | [diff] [blame] | 382 | final int titleResId = sa.getResourceId(R.styleable.Shortcut_shortcutShortLabel, 0); |
| 383 | final int textResId = sa.getResourceId(R.styleable.Shortcut_shortcutLongLabel, 0); |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 384 | final int disabledMessageResId = sa.getResourceId( |
| 385 | R.styleable.Shortcut_shortcutDisabledMessage, 0); |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 386 | |
| 387 | if (TextUtils.isEmpty(id)) { |
Makoto Onuki | 6771d73 | 2016-07-14 12:58:40 -0700 | [diff] [blame] | 388 | Log.w(TAG, "android:shortcutId must be provided. activity=" + activity); |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 389 | return null; |
| 390 | } |
| 391 | if (titleResId == 0) { |
Makoto Onuki | 6771d73 | 2016-07-14 12:58:40 -0700 | [diff] [blame] | 392 | Log.w(TAG, "android:shortcutShortLabel must be provided. activity=" + activity); |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 393 | return null; |
| 394 | } |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 395 | |
| 396 | return createShortcutFromManifest( |
| 397 | service, |
| 398 | userId, |
| 399 | id, |
| 400 | packageName, |
| 401 | activity, |
| 402 | titleResId, |
| 403 | textResId, |
| 404 | disabledMessageResId, |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 405 | rank, |
| 406 | iconResId, |
| 407 | enabled); |
| 408 | } finally { |
| 409 | sa.recycle(); |
| 410 | } |
| 411 | } |
| 412 | |
| 413 | private static ShortcutInfo createShortcutFromManifest(ShortcutService service, |
| 414 | @UserIdInt int userId, String id, String packageName, ComponentName activityComponent, |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 415 | int titleResId, int textResId, int disabledMessageResId, |
| 416 | int rank, int iconResId, boolean enabled) { |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 417 | |
| 418 | final int flags = |
| 419 | (enabled ? ShortcutInfo.FLAG_MANIFEST : ShortcutInfo.FLAG_DISABLED) |
| 420 | | ShortcutInfo.FLAG_IMMUTABLE |
| 421 | | ((iconResId != 0) ? ShortcutInfo.FLAG_HAS_ICON_RES : 0); |
Makoto Onuki | a4f89b1 | 2017-10-05 10:37:55 -0700 | [diff] [blame] | 422 | final int disabledReason = |
| 423 | enabled ? ShortcutInfo.DISABLED_REASON_NOT_DISABLED |
| 424 | : ShortcutInfo.DISABLED_REASON_BY_APP; |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 425 | |
Makoto Onuki | 157b162 | 2016-06-02 16:13:10 -0700 | [diff] [blame] | 426 | // Note we don't need to set resource names here yet. They'll be set when they're about |
| 427 | // to be published. |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 428 | return new ShortcutInfo( |
| 429 | userId, |
| 430 | id, |
| 431 | packageName, |
| 432 | activityComponent, |
| 433 | null, // icon |
| 434 | null, // title string |
| 435 | titleResId, |
Makoto Onuki | 157b162 | 2016-06-02 16:13:10 -0700 | [diff] [blame] | 436 | null, // title res name |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 437 | null, // text string |
| 438 | textResId, |
Makoto Onuki | 157b162 | 2016-06-02 16:13:10 -0700 | [diff] [blame] | 439 | null, // text res name |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 440 | null, // disabled message string |
| 441 | disabledMessageResId, |
Makoto Onuki | 157b162 | 2016-06-02 16:13:10 -0700 | [diff] [blame] | 442 | null, // disabled message res name |
Makoto Onuki | df6da04 | 2016-06-16 09:51:40 -0700 | [diff] [blame] | 443 | null, // categories |
| 444 | null, // intent |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 445 | rank, |
| 446 | null, // extras |
| 447 | service.injectCurrentTimeMillis(), |
| 448 | flags, |
| 449 | iconResId, |
Makoto Onuki | 157b162 | 2016-06-02 16:13:10 -0700 | [diff] [blame] | 450 | null, // icon res name |
Makoto Onuki | a4f89b1 | 2017-10-05 10:37:55 -0700 | [diff] [blame] | 451 | null, // bitmap path |
| 452 | disabledReason); |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 453 | } |
Mehdi Alizadeh | 3277462 | 2018-11-05 17:32:01 -0800 | [diff] [blame] | 454 | |
| 455 | private static String parseCategory(ShortcutService service, AttributeSet attrs) { |
| 456 | final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, |
| 457 | R.styleable.IntentCategory); |
| 458 | try { |
| 459 | if (sa.getType(R.styleable.IntentCategory_name) != TypedValue.TYPE_STRING) { |
| 460 | Log.w(TAG, "android:name must be string literal."); |
| 461 | return null; |
| 462 | } |
| 463 | return sa.getString(R.styleable.IntentCategory_name); |
| 464 | } finally { |
| 465 | sa.recycle(); |
| 466 | } |
| 467 | } |
| 468 | |
| 469 | private static ShareTargetInfo parseShareTargetAttributes(ShortcutService service, |
| 470 | AttributeSet attrs) { |
| 471 | final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, |
| 472 | R.styleable.Intent); |
| 473 | try { |
| 474 | String targetClass = sa.getString(R.styleable.Intent_targetClass); |
| 475 | if (TextUtils.isEmpty(targetClass)) { |
| 476 | Log.w(TAG, "android:targetClass must be provided."); |
| 477 | return null; |
| 478 | } |
| 479 | return new ShareTargetInfo(null, targetClass, null); |
| 480 | } finally { |
| 481 | sa.recycle(); |
| 482 | } |
| 483 | } |
| 484 | |
| 485 | private static ShareTargetInfo.TargetData parseShareTargetData(ShortcutService service, |
| 486 | AttributeSet attrs) { |
| 487 | final TypedArray sa = service.mContext.getResources().obtainAttributes(attrs, |
| 488 | R.styleable.AndroidManifestData); |
| 489 | try { |
| 490 | if (sa.getType(R.styleable.AndroidManifestData_mimeType) != TypedValue.TYPE_STRING) { |
| 491 | Log.w(TAG, "android:mimeType must be string literal."); |
| 492 | return null; |
| 493 | } |
| 494 | String scheme = sa.getString(R.styleable.AndroidManifestData_scheme); |
| 495 | String host = sa.getString(R.styleable.AndroidManifestData_host); |
| 496 | String port = sa.getString(R.styleable.AndroidManifestData_port); |
| 497 | String path = sa.getString(R.styleable.AndroidManifestData_path); |
| 498 | String pathPattern = sa.getString(R.styleable.AndroidManifestData_pathPattern); |
| 499 | String pathPrefix = sa.getString(R.styleable.AndroidManifestData_pathPrefix); |
| 500 | String mimeType = sa.getString(R.styleable.AndroidManifestData_mimeType); |
| 501 | return new ShareTargetInfo.TargetData(scheme, host, port, path, pathPattern, pathPrefix, |
| 502 | mimeType); |
| 503 | } finally { |
| 504 | sa.recycle(); |
| 505 | } |
| 506 | } |
Makoto Onuki | 22fcc68 | 2016-05-17 14:52:19 -0700 | [diff] [blame] | 507 | } |