OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2017 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 package org.chromium.content.browser.installedapp; | |
6 | |
7 import android.content.Context; | |
8 import android.content.pm.ApplicationInfo; | |
9 import android.content.pm.PackageManager; | |
10 import android.content.pm.PackageManager.NameNotFoundException; | |
11 import android.content.res.Resources; | |
12 | |
13 import org.json.JSONArray; | |
14 import org.json.JSONException; | |
15 import org.json.JSONObject; | |
16 | |
17 import org.chromium.base.Log; | |
18 import org.chromium.installedapp.mojom.InstalledAppProvider; | |
19 import org.chromium.installedapp.mojom.RelatedApplication; | |
20 import org.chromium.mojo.system.MojoException; | |
21 | |
22 import java.net.URI; | |
23 import java.net.URISyntaxException; | |
24 import java.util.ArrayList; | |
25 | |
26 /** | |
27 * Android implementation of the InstalledAppProvider service defined in | |
28 * content/common/installedapp/installed_app_provider.mojom. | |
boliu
2017/03/17 18:02:55
cs says this is in third_party/WebKit?
Matt Giuca
2017/03/20 06:31:56
Done. (Removed the full path, because otherwise th
| |
29 */ | |
30 public class InstalledAppProviderImpl implements InstalledAppProvider { | |
31 private static final String ASSET_STATEMENTS_KEY = "asset_statements"; | |
32 private static final String ASSET_STATEMENT_FIELD_TARGET = "target"; | |
33 private static final String ASSET_STATEMENT_FIELD_NAMESPACE = "namespace"; | |
34 private static final String ASSET_STATEMENT_FIELD_SITE = "site"; | |
35 private static final String ASSET_STATEMENT_NAMESPACE_WEB = "web"; | |
36 private static final String RELATED_APP_PLATFORM_ANDROID = "play"; | |
37 | |
38 private static final String TAG = "InstalledAppProvider"; | |
39 | |
40 private final Context mContext; | |
41 private final URI mPageUrl; | |
42 | |
43 public InstalledAppProviderImpl(String pageUrl, Context context) { | |
44 mContext = context; | |
45 | |
46 try { | |
47 mPageUrl = new URI(pageUrl); | |
48 } catch (URISyntaxException e) { | |
49 throw new AssertionError(e); | |
50 } | |
51 } | |
52 | |
53 @Override | |
54 public void filterInstalledApps( | |
55 RelatedApplication[] relatedApps, FilterInstalledAppsResponse callba ck) { | |
56 ArrayList<RelatedApplication> installedApps = new ArrayList<RelatedAppli cation>(); | |
57 PackageManager pm = mContext.getPackageManager(); | |
58 for (RelatedApplication app : relatedApps) { | |
59 // If the package is of type "play", it is installed, and the origin is associated with | |
60 // package, add the package to the list of valid packages. | |
61 // NOTE: For security, it must not be possible to distinguish (from the response) | |
62 // between the app not being installed and the origin not being asso ciated with the app | |
63 // (otherwise, arbitrary websites would be able to test whether un-a ssociated apps are | |
64 // installed on the user's device). | |
65 if (app.platform.equals(RELATED_APP_PLATFORM_ANDROID) && app.id != n ull | |
boliu
2017/03/17 18:02:55
A nit that reversing the equals call is safer sinc
Matt Giuca
2017/03/20 06:31:56
Yes, Mojo guarantees platform is not null as it is
| |
66 && isAppInstalledAndAssociatedWithOrigin(app.id, mPageUrl, p m)) { | |
67 installedApps.add(app); | |
68 } | |
69 } | |
70 RelatedApplication[] installedAppsArray = new RelatedApplication[install edApps.size()]; | |
71 installedApps.toArray(installedAppsArray); | |
72 callback.call(installedAppsArray); | |
73 } | |
74 | |
75 @Override | |
76 public void close() {} | |
77 | |
78 @Override | |
79 public void onConnectionError(MojoException e) {} | |
80 | |
81 /** | |
82 * Determines whether a particular app is installed and matches the origin. | |
83 * | |
84 * @param packageName Name of the Android package to check if installed. Ret urns false if the | |
85 * app is not installed. | |
86 * @param pageUrl Returns false if the Android package does not declare asso ciation with the | |
87 * origin of this URL. | |
88 */ | |
89 private static boolean isAppInstalledAndAssociatedWithOrigin( | |
90 String packageName, URI pageUrl, PackageManager pm) { | |
91 // Early-exit if the Android app is not installed. | |
92 JSONArray statements; | |
93 try { | |
94 statements = getAssetStatements(packageName, pm); | |
95 } catch (NameNotFoundException e) { | |
96 return false; | |
97 } | |
98 | |
99 // The installed Android app has provided us with a list of asset statem ents. If any one of | |
100 // those statements is a web asset that matches the given origin, return true. | |
101 for (int i = 0; i < statements.length(); i++) { | |
102 JSONObject statement; | |
103 try { | |
104 statement = statements.getJSONObject(i); | |
105 } catch (JSONException e) { | |
106 // If an element is not an object, just ignore it. | |
107 continue; | |
108 } | |
109 | |
110 URI site = getSiteForWebAsset(statement); | |
111 | |
112 // The URI is considered equivalent if the scheme, host, and port ma tch, according | |
113 // to the DigitalAssetLinks v1 spec. | |
114 if (site != null && statementTargetMatches(pageUrl, site)) { | |
115 return true; | |
116 } | |
117 } | |
118 | |
119 // No asset matched the origin. | |
120 return false; | |
121 } | |
122 | |
123 /** | |
124 * Gets the asset statements from an Android app's manifest. | |
125 * | |
126 * This retrieves the list of statements from the Android app's "asset_state ments" manifest | |
127 * resource, as specified in Digital Asset Links v1. | |
128 * | |
129 * @param packageName Name of the Android package to get statements from. | |
130 * @return The list of asset statements, parsed from JSON. | |
131 * @throws NameNotFoundException if the application is not installed. | |
132 */ | |
133 private static JSONArray getAssetStatements(String packageName, PackageManag er pm) | |
134 throws NameNotFoundException { | |
135 // Get the <meta-data> from this app's manifest. | |
136 // Throws NameNotFoundException if the application is not installed. | |
137 ApplicationInfo appInfo = pm.getApplicationInfo(packageName, PackageMana ger.GET_META_DATA); | |
138 int identifier = appInfo.metaData.getInt(ASSET_STATEMENTS_KEY); | |
139 if (identifier == 0) { | |
140 return new JSONArray(); | |
141 } | |
142 | |
143 // Throws NameNotFoundException in the rare case that the application wa s uninstalled since | |
144 // getting |appInfo| (or resources could not be loaded for some other re ason). | |
145 Resources resources = pm.getResourcesForApplication(appInfo); | |
146 | |
147 String statements; | |
148 try { | |
149 statements = resources.getString(identifier); | |
150 } catch (Resources.NotFoundException e) { | |
151 // This should never happen, but it could if there was a broken APK, so handle it | |
152 // gracefully without crashing. | |
153 Log.w(TAG, | |
154 "Android package " + packageName + " missing asset statement s resource (0x" | |
155 + Integer.toHexString(identifier) + ")."); | |
156 return new JSONArray(); | |
157 } | |
158 | |
159 try { | |
160 return new JSONArray(statements); | |
161 } catch (JSONException e) { | |
162 // If the JSON is invalid or not an array, assume it is empty. | |
163 Log.w(TAG, | |
164 "Android package " + packageName | |
165 + " has JSON syntax error in asset statements resour ce (0x" | |
166 + Integer.toHexString(identifier) + ")."); | |
167 return new JSONArray(); | |
168 } | |
169 } | |
170 | |
171 /** | |
172 * Gets the "site" URI from an Android asset statement. | |
173 * | |
174 * @return The site, or null if the asset string was invalid or not related to a web site. This | |
175 * could be because: the JSON string was invalid, there was no "targ et" field, this was | |
176 * not a web asset, there was no "site" field, or the "site" field w as invalid. | |
177 */ | |
178 private static URI getSiteForWebAsset(JSONObject statement) { | |
179 JSONObject target; | |
180 try { | |
181 // Ignore the "relation" field and allow an asset with any relation to this origin. | |
182 // TODO(mgiuca): [Spec issue] Should we require a specific relation string, rather | |
183 // than any or no relation? | |
184 target = statement.getJSONObject(ASSET_STATEMENT_FIELD_TARGET); | |
185 } catch (JSONException e) { | |
186 return null; | |
187 } | |
188 | |
189 // If it is not a web asset, skip it. | |
190 if (!isAssetWeb(target)) { | |
191 return null; | |
192 } | |
193 | |
194 try { | |
195 return new URI(target.getString(ASSET_STATEMENT_FIELD_SITE)); | |
196 } catch (JSONException e) { | |
boliu
2017/03/17 18:02:56
nit: you can do "JSONException | URISyntaxExceptio
Matt Giuca
2017/03/20 06:31:56
Done.
Nice, I didn't know that.
| |
197 return null; | |
198 } catch (URISyntaxException e) { | |
199 return null; | |
200 } | |
201 } | |
202 | |
203 /** | |
204 * Determines whether an Android asset statement is for a website. | |
205 * | |
206 * @param target The "target" field of the asset statement. | |
207 */ | |
208 private static boolean isAssetWeb(JSONObject target) { | |
209 String namespace; | |
210 try { | |
211 namespace = target.getString(ASSET_STATEMENT_FIELD_NAMESPACE); | |
212 } catch (JSONException e) { | |
213 return false; | |
214 } | |
215 | |
216 return namespace.equals(ASSET_STATEMENT_NAMESPACE_WEB); | |
217 } | |
218 | |
219 private static boolean statementTargetMatches(URI pageUrl, URI assetUrl) { | |
220 if (assetUrl.getScheme() == null || assetUrl.getAuthority() == null) { | |
221 return false; | |
222 } | |
223 | |
224 return assetUrl.getScheme().equals(pageUrl.getScheme()) | |
225 && assetUrl.getAuthority().equals(pageUrl.getAuthority()); | |
226 } | |
227 } | |
OLD | NEW |