1 // Copyright 2016 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.chrome.browser.download;
6 
7 import android.app.Activity;
8 import android.app.DownloadManager;
9 import android.content.ActivityNotFoundException;
10 import android.content.Context;
11 import android.content.Intent;
12 import android.content.pm.PackageInfo;
13 import android.content.pm.PackageManager;
14 import android.net.Uri;
15 import android.os.Build;
16 import android.text.TextUtils;
17 
18 import androidx.annotation.MainThread;
19 import androidx.annotation.Nullable;
20 
21 import org.chromium.base.ApplicationStatus;
22 import org.chromium.base.ContentUriUtils;
23 import org.chromium.base.ContextUtils;
24 import org.chromium.base.FileUtils;
25 import org.chromium.base.IntentUtils;
26 import org.chromium.base.Log;
27 import org.chromium.base.annotations.CalledByNative;
28 import org.chromium.base.annotations.NativeMethods;
29 import org.chromium.base.metrics.RecordHistogram;
30 import org.chromium.base.metrics.RecordUserAction;
31 import org.chromium.chrome.R;
32 import org.chromium.chrome.browser.ChromeTabbedActivity;
33 import org.chromium.chrome.browser.IntentHandler;
34 import org.chromium.chrome.browser.document.ChromeIntentUtil;
35 import org.chromium.chrome.browser.download.items.OfflineContentAggregatorFactory;
36 import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
37 import org.chromium.chrome.browser.flags.ChromeFeatureList;
38 import org.chromium.chrome.browser.media.MediaViewerUtils;
39 import org.chromium.chrome.browser.offlinepages.DownloadUiActionFlags;
40 import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
41 import org.chromium.chrome.browser.offlinepages.OfflinePageOrigin;
42 import org.chromium.chrome.browser.offlinepages.OfflinePageUtils;
43 import org.chromium.chrome.browser.offlinepages.downloads.OfflinePageDownloadBridge;
44 import org.chromium.chrome.browser.profiles.Profile;
45 import org.chromium.chrome.browser.tab.Tab;
46 import org.chromium.chrome.browser.tab.TabLaunchType;
47 import org.chromium.chrome.browser.tabmodel.document.TabDelegate;
48 import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
49 import org.chromium.components.download.DownloadState;
50 import org.chromium.components.download.ResumeMode;
51 import org.chromium.components.embedder_support.util.UrlConstants;
52 import org.chromium.components.feature_engagement.EventConstants;
53 import org.chromium.components.feature_engagement.Tracker;
54 import org.chromium.components.offline_items_collection.ContentId;
55 import org.chromium.components.offline_items_collection.FailState;
56 import org.chromium.components.offline_items_collection.LaunchLocation;
57 import org.chromium.components.offline_items_collection.LegacyHelpers;
58 import org.chromium.components.offline_items_collection.OfflineItem;
59 import org.chromium.components.offline_items_collection.OpenParams;
60 import org.chromium.content_public.browser.BrowserStartupController;
61 import org.chromium.content_public.browser.LoadUrlParams;
62 import org.chromium.ui.base.DeviceFormFactor;
63 import org.chromium.ui.widget.Toast;
64 
65 import java.io.File;
66 
67 /**
68  * A class containing some utility static methods.
69  */
70 public class DownloadUtils {
71     private static final String TAG = "download";
72 
73     private static final String EXTRA_IS_OFF_THE_RECORD =
74             "org.chromium.chrome.browser.download.IS_OFF_THE_RECORD";
75     private static final String MIME_TYPE_ZIP = "application/zip";
76     private static final String DOCUMENTS_UI_PACKAGE_NAME = "com.android.documentsui";
77     public static final String EXTRA_SHOW_PREFETCHED_CONTENT =
78             "org.chromium.chrome.browser.download.SHOW_PREFETCHED_CONTENT";
79 
80     /**
81      * Displays the download manager UI. Note the UI is different on tablets and on phones.
82      * @param activity The current activity is available.
83      * @param tab The current tab if it exists.
84      * @param source The source where the user action is coming from.
85      * @return Whether the UI was shown.
86      */
showDownloadManager( @ullable Activity activity, @Nullable Tab tab, @DownloadOpenSource int source)87     public static boolean showDownloadManager(
88             @Nullable Activity activity, @Nullable Tab tab, @DownloadOpenSource int source) {
89         return showDownloadManager(activity, tab, source, false);
90     }
91 
92     /**
93      * Displays the download manager UI. Note the UI is different on tablets and on phones.
94      * @param activity The current activity is available.
95      * @param tab The current tab if it exists.
96      * @param source The source where the user action is coming from.
97      * @param showPrefetchedContent Whether the manager should start with prefetched content section
98      * expanded.
99      * @return Whether the UI was shown.
100      */
101     @CalledByNative
showDownloadManager(@ullable Activity activity, @Nullable Tab tab, @DownloadOpenSource int source, boolean showPrefetchedContent)102     public static boolean showDownloadManager(@Nullable Activity activity, @Nullable Tab tab,
103             @DownloadOpenSource int source, boolean showPrefetchedContent) {
104         // Figure out what tab was last being viewed by the user.
105         if (activity == null) activity = ApplicationStatus.getLastTrackedFocusedActivity();
106         Context appContext = ContextUtils.getApplicationContext();
107         boolean isTablet;
108 
109         if (tab == null && activity instanceof ChromeTabbedActivity) {
110             ChromeTabbedActivity chromeActivity = ((ChromeTabbedActivity) activity);
111             tab = chromeActivity.getActivityTab();
112             isTablet = chromeActivity.isTablet();
113         } else {
114             Context displayContext = activity != null ? activity : appContext;
115             isTablet = DeviceFormFactor.isNonMultiDisplayContextOnTablet(displayContext);
116         }
117 
118         if (isTablet) {
119             // Download Home shows up as a tab on tablets.
120             LoadUrlParams params = new LoadUrlParams(UrlConstants.DOWNLOADS_URL);
121             if (tab == null || !tab.isInitialized()) {
122                 // Open a new tab, which pops Chrome into the foreground.
123                 TabDelegate delegate = new TabDelegate(false);
124                 delegate.createNewTab(params, TabLaunchType.FROM_CHROME_UI, null);
125             } else {
126                 // Download Home shows up inside an existing tab, but only if the last Activity was
127                 // the ChromeTabbedActivity.
128                 tab.loadUrl(params);
129 
130                 // Bring Chrome to the foreground, if possible.
131                 Intent intent = ChromeIntentUtil.createBringTabToFrontIntent(tab.getId());
132                 if (intent != null) {
133                     intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
134                     IntentUtils.safeStartActivity(appContext, intent);
135                 }
136             }
137         } else {
138             // Download Home shows up as a new Activity on phones.
139             Intent intent = new Intent();
140             intent.setClass(appContext, DownloadActivity.class);
141             intent.putExtra(EXTRA_SHOW_PREFETCHED_CONTENT, showPrefetchedContent);
142             if (tab != null) intent.putExtra(EXTRA_IS_OFF_THE_RECORD, tab.isIncognito());
143             if (activity == null) {
144                 // Stands alone in its own task.
145                 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
146                 appContext.startActivity(intent);
147             } else {
148                 // Sits on top of another Activity.
149                 intent.addFlags(
150                         Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
151                 intent.putExtra(IntentHandler.EXTRA_PARENT_COMPONENT, activity.getComponentName());
152                 activity.startActivity(intent);
153             }
154         }
155 
156         if (BrowserStartupController.getInstance().isFullBrowserStarted()) {
157             // TODO (https://crbug.com/1048632): Use the current profile (i.e., regular profile or
158             // incognito profile) instead of always using regular profile. It works correctly now,
159             // but it is not safe.
160             Profile profile = (tab == null ? Profile.getLastUsedRegularProfile()
161                                            : Profile.fromWebContents(tab.getWebContents()));
162             Tracker tracker = TrackerFactory.getTrackerForProfile(profile);
163             tracker.notifyEvent(EventConstants.DOWNLOAD_HOME_OPENED);
164         }
165         DownloadMetrics.recordDownloadPageOpen(source);
166         return true;
167     }
168 
169     /**
170      * @return Whether or not the Intent corresponds to a DownloadActivity that should show off the
171      *         record downloads.
172      */
shouldShowOffTheRecordDownloads(Intent intent)173     public static boolean shouldShowOffTheRecordDownloads(Intent intent) {
174         return IntentUtils.safeGetBooleanExtra(intent, EXTRA_IS_OFF_THE_RECORD, false);
175     }
176 
177     /**
178      * @return Whether or not the prefetched content section should be expanded on launch of the
179      * DownloadActivity.
180      */
shouldShowPrefetchContent(Intent intent)181     public static boolean shouldShowPrefetchContent(Intent intent) {
182         return IntentUtils.safeGetBooleanExtra(intent, EXTRA_SHOW_PREFETCHED_CONTENT, false);
183     }
184 
185     /**
186      * @return Whether or not pagination headers should be shown on download home.
187      */
shouldShowPaginationHeaders()188     public static boolean shouldShowPaginationHeaders() {
189         return ChromeAccessibilityUtil.get().isAccessibilityEnabled()
190                 || ChromeAccessibilityUtil.isHardwareKeyboardAttached(
191                         ContextUtils.getApplicationContext().getResources().getConfiguration());
192     }
193 
194     /**
195      * Records metrics related to downloading a page. Should be called after a tap on the download
196      * page button.
197      * @param tab The Tab containing the page being downloaded.
198      */
recordDownloadPageMetrics(Tab tab)199     public static void recordDownloadPageMetrics(Tab tab) {
200         RecordHistogram.recordPercentageHistogram(
201                 "OfflinePages.SavePage.PercentLoaded", Math.round(tab.getProgress() * 100));
202     }
203 
204     /**
205      * Shows a "Downloading..." toast. Should be called after a download has been started.
206      * @param context The {@link Context} used to make the toast.
207      */
showDownloadStartToast(Context context)208     public static void showDownloadStartToast(Context context) {
209         Toast.makeText(context, R.string.download_started, Toast.LENGTH_SHORT).show();
210     }
211 
212     /**
213      * Issues a request to the {@link DownloadManagerService} associated to check for externally
214      * removed downloads.
215      * See {@link DownloadManagerService#checkForExternallyRemovedDownloads}.
216      * @param isOffTheRecord  Whether to check downloads for the off the record profile.
217      */
checkForExternallyRemovedDownloads(boolean isOffTheRecord)218     public static void checkForExternallyRemovedDownloads(boolean isOffTheRecord) {
219         if (ChromeFeatureList.isEnabled(ChromeFeatureList.DOWNLOAD_OFFLINE_CONTENT_PROVIDER)) {
220             return;
221         }
222 
223         if (isOffTheRecord) {
224             DownloadManagerService.getDownloadManagerService().checkForExternallyRemovedDownloads(
225                     true);
226         }
227         DownloadManagerService.getDownloadManagerService().checkForExternallyRemovedDownloads(
228                 false);
229         RecordUserAction.record(
230                 "Android.DownloadManager.CheckForExternallyRemovedItems");
231     }
232 
233     /**
234      * Trigger the download of an Offline Page.
235      * @param context Context to pull resources from.
236      */
downloadOfflinePage(Context context, Tab tab)237     public static void downloadOfflinePage(Context context, Tab tab) {
238         OfflinePageOrigin origin = new OfflinePageOrigin(context, tab);
239 
240         if (tab.isShowingErrorPage()) {
241             // The download needs to be scheduled to happen at later time due to current network
242             // error.
243             final OfflinePageBridge bridge =
244                     OfflinePageBridge.getForProfile(Profile.fromWebContents(tab.getWebContents()));
245             bridge.scheduleDownload(tab.getWebContents(), OfflinePageBridge.ASYNC_NAMESPACE,
246                     tab.getUrlString(), DownloadUiActionFlags.PROMPT_DUPLICATE, origin);
247         } else {
248             // Otherwise, the download can be started immediately.
249             OfflinePageDownloadBridge.startDownload(tab, origin);
250             DownloadUtils.recordDownloadPageMetrics(tab);
251         }
252 
253         Tracker tracker =
254                 TrackerFactory.getTrackerForProfile(Profile.fromWebContents(tab.getWebContents()));
255         tracker.notifyEvent(EventConstants.DOWNLOAD_PAGE_STARTED);
256     }
257 
258     /**
259      * Whether the user should be allowed to download the current page.
260      * @param tab Tab displaying the page that will be downloaded.
261      * @return    Whether the "Download Page" button should be enabled.
262      */
isAllowedToDownloadPage(Tab tab)263     public static boolean isAllowedToDownloadPage(Tab tab) {
264         if (tab == null) return false;
265 
266         // Offline pages isn't supported in Incognito. This should be checked before calling
267         // OfflinePageBridge.getForProfile because OfflinePageBridge instance will not be found
268         // for incognito profile.
269         if (tab.isIncognito()) return false;
270 
271         // Check if the page url is supported for saving. Only HTTP and HTTPS pages are allowed.
272         if (!OfflinePageBridge.canSavePage(tab.getUrlString())) return false;
273 
274         // Download will only be allowed for the error page if download button is shown in the page.
275         if (tab.isShowingErrorPage()) {
276             final OfflinePageBridge bridge =
277                     OfflinePageBridge.getForProfile(Profile.fromWebContents(tab.getWebContents()));
278             return bridge.isShowingDownloadButtonInErrorPage(tab.getWebContents());
279         }
280 
281         // Don't allow re-downloading the currently displayed offline page.
282         if (OfflinePageUtils.isOfflinePage(tab)) return false;
283 
284         return true;
285     }
286 
287     /**
288      * Returns a URI that points at the file.
289      * @param filePath File path to get a URI for.
290      * @return URI that points at that file, either as a content:// URI or a file:// URI.
291      */
292     @MainThread
getUriForItem(String filePath)293     public static Uri getUriForItem(String filePath) {
294         if (ContentUriUtils.isContentUri(filePath)) return Uri.parse(filePath);
295 
296         // It's ok to use blocking calls on main thread here, since the user is waiting to open or
297         // share the file to other apps.
298         boolean isOnSDCard = DownloadDirectoryProvider.isDownloadOnSDCard(filePath);
299         if (ChromeFeatureList.isEnabled(ChromeFeatureList.DOWNLOAD_FILE_PROVIDER) && isOnSDCard) {
300             // Use custom file provider to generate content URI for download on SD card.
301             return DownloadFileProvider.createContentUri(filePath);
302         }
303         // Use FileProvider to generate content URI or file URI.
304         return FileUtils.getUriForFile(new File(filePath));
305     }
306 
307     /**
308      * Get the URI when shared or opened by other apps.
309      *
310      * @param filePath Downloaded file path.
311      * @return URI for other apps to use the file via {@link android.content.ContentResolver}.
312      */
getUriForOtherApps(String filePath)313     public static Uri getUriForOtherApps(String filePath) {
314         // Some old Samsung devices with Android M- must use file URI. See https://crbug.com/705748.
315         return Build.VERSION.SDK_INT > Build.VERSION_CODES.M ? getUriForItem(filePath)
316                                                              : Uri.fromFile(new File(filePath));
317     }
318 
319     @CalledByNative
getUriStringForPath(String filePath)320     private static String getUriStringForPath(String filePath) {
321         if (ContentUriUtils.isContentUri(filePath)) return filePath;
322         Uri uri = getUriForItem(filePath);
323         return uri != null ? uri.toString() : new String();
324     }
325 
326     /**
327      * Utility method to open an {@link OfflineItem}, which can be a chrome download, offline page.
328      * Falls back to open download home.
329      * @param contentId The {@link ContentId} of the associated offline item.
330      * @param isOffTheRecord Whether the download should be opened in incognito mode.
331      * @param source The location from which the download was opened.
332      */
openItem( ContentId contentId, boolean isOffTheRecord, @DownloadOpenSource int source)333     public static void openItem(
334             ContentId contentId, boolean isOffTheRecord, @DownloadOpenSource int source) {
335         if (LegacyHelpers.isLegacyAndroidDownload(contentId)) {
336             ContextUtils.getApplicationContext().startActivity(
337                     new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
338                             .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
339         } else if (LegacyHelpers.isLegacyOfflinePage(contentId)) {
340             OpenParams openParams = new OpenParams(LaunchLocation.PROGRESS_BAR);
341             openParams.openInIncognito = isOffTheRecord;
342             OfflineContentAggregatorFactory.get().openItem(openParams, contentId);
343         } else {
344             DownloadManagerService.getDownloadManagerService().openDownload(
345                     contentId, isOffTheRecord, source);
346         }
347     }
348 
349     /**
350      * Opens a file in Chrome or in another app if appropriate.
351      * @param filePath Path to the file to open, can be a content Uri.
352      * @param mimeType mime type of the file.
353      * @param downloadGuid The associated download GUID.
354      * @param isOffTheRecord whether we are in an off the record context.
355      * @param originalUrl The original url of the downloaded file.
356      * @param referrer Referrer of the downloaded file.
357      * @param source The source that tries to open the download file.
358      * @return whether the file could successfully be opened.
359      */
openFile(String filePath, String mimeType, String downloadGuid, boolean isOffTheRecord, String originalUrl, String referrer, @DownloadOpenSource int source)360     public static boolean openFile(String filePath, String mimeType, String downloadGuid,
361             boolean isOffTheRecord, String originalUrl, String referrer,
362             @DownloadOpenSource int source) {
363         DownloadMetrics.recordDownloadOpen(source, mimeType);
364         Context context = ContextUtils.getApplicationContext();
365         DownloadManagerService service = DownloadManagerService.getDownloadManagerService();
366 
367         // Check if Chrome should open the file itself.
368         if (service.isDownloadOpenableInBrowser(isOffTheRecord, mimeType)) {
369             // Share URIs use the content:// scheme when able, which looks bad when displayed
370             // in the URL bar.
371             Uri contentUri = getUriForItem(filePath);
372             Uri fileUri = contentUri;
373             if (!ContentUriUtils.isContentUri(filePath)) {
374                 File file = new File(filePath);
375                 fileUri = Uri.fromFile(file);
376             }
377             String normalizedMimeType = Intent.normalizeMimeType(mimeType);
378 
379             Intent intent = MediaViewerUtils.getMediaViewerIntent(fileUri /*displayUri*/,
380                     contentUri /*contentUri*/, normalizedMimeType,
381                     true /* allowExternalAppHandlers */);
382             IntentHandler.startActivityForTrustedIntent(intent);
383             service.updateLastAccessTime(downloadGuid, isOffTheRecord);
384             return true;
385         }
386 
387         // Check if any apps can open the file.
388         try {
389             // TODO(qinmin): Move this to an AsyncTask so we don't need to temper with strict mode.
390             Uri uri = ContentUriUtils.isContentUri(filePath) ? Uri.parse(filePath)
391                                                              : getUriForOtherApps(filePath);
392             Intent viewIntent =
393                     MediaViewerUtils.createViewIntentForUri(uri, mimeType, originalUrl, referrer);
394             context.startActivity(viewIntent);
395             service.updateLastAccessTime(downloadGuid, isOffTheRecord);
396             return true;
397         } catch (Exception e) {
398             Log.e(TAG, "Cannot start activity to open file", e);
399         }
400 
401         // If this is a zip file, check if Android Files app exists.
402         if (MIME_TYPE_ZIP.equals(mimeType)) {
403             try {
404                 PackageInfo packageInfo = context.getPackageManager().getPackageInfo(
405                         DOCUMENTS_UI_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
406                 if (packageInfo != null) {
407                     Intent viewDownloadsIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
408                     viewDownloadsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
409                     viewDownloadsIntent.setPackage(DOCUMENTS_UI_PACKAGE_NAME);
410                     context.startActivity(viewDownloadsIntent);
411                     return true;
412                 }
413             } catch (Exception e) {
414                 Log.e(TAG, "Cannot find files app for openning zip files", e);
415             }
416         }
417         // Can't launch the Intent.
418         if (source != DownloadOpenSource.DOWNLOAD_PROGRESS_INFO_BAR) {
419             Toast.makeText(context, context.getString(R.string.download_cant_open_file),
420                          Toast.LENGTH_SHORT)
421                     .show();
422         }
423         return false;
424     }
425 
426     @CalledByNative
openDownload(String filePath, String mimeType, String downloadGuid, boolean isOffTheRecord, String originalUrl, String referer, @DownloadOpenSource int source)427     private static void openDownload(String filePath, String mimeType, String downloadGuid,
428             boolean isOffTheRecord, String originalUrl, String referer,
429             @DownloadOpenSource int source) {
430         boolean canOpen = DownloadUtils.openFile(
431                 filePath, mimeType, downloadGuid, isOffTheRecord, originalUrl, referer, source);
432         if (!canOpen) {
433             DownloadUtils.showDownloadManager(null, null, source);
434         }
435     }
436 
437     /**
438      * Fires an Intent to open a downloaded item.
439      * @param context Context to use.
440      * @param intent  Intent that can be fired.
441      * @return Whether an Activity was successfully started for the Intent.
442      */
fireOpenIntentForDownload(Context context, Intent intent)443     static boolean fireOpenIntentForDownload(Context context, Intent intent) {
444         try {
445             if (TextUtils.equals(intent.getPackage(), context.getPackageName())) {
446                 IntentHandler.startActivityForTrustedIntent(intent);
447             } else {
448                 context.startActivity(intent);
449             }
450             return true;
451         } catch (ActivityNotFoundException ex) {
452             Log.d(TAG, "Activity not found for " + intent.getType() + " over "
453                     + intent.getData().getScheme(), ex);
454         } catch (SecurityException ex) {
455             Log.d(TAG, "cannot open intent: " + intent, ex);
456         } catch (Exception ex) {
457             Log.d(TAG, "cannot open intent: " + intent, ex);
458         }
459 
460         return false;
461     }
462 
463     /**
464      * Get the resume mode based on the current fail state, to distinguish the case where download
465      * cannot be resumed at all or can be resumed in the middle, or should be restarted from the
466      * beginning.
467      * @param url URL of the download.
468      * @param failState Why the download failed.
469      * @return The resume mode for the current fail state.
470      */
getResumeMode(String url, @FailState int failState)471     public static @ResumeMode int getResumeMode(String url, @FailState int failState) {
472         return DownloadUtilsJni.get().getResumeMode(url, failState);
473     }
474 
475     /**
476      * Query the Download backends about whether a download is paused.
477      *
478      * The Java-side contains more information about the status of a download than is persisted
479      * by the native backend, so it is queried first.
480      *
481      * @param item Download to check the status of.
482      * @return Whether the download is paused or not.
483      */
isDownloadPaused(DownloadItem item)484     public static boolean isDownloadPaused(DownloadItem item) {
485         DownloadSharedPreferenceHelper helper = DownloadSharedPreferenceHelper.getInstance();
486         DownloadSharedPreferenceEntry entry =
487                 helper.getDownloadSharedPreferenceEntry(item.getContentId());
488 
489         if (entry != null) {
490             // The Java downloads backend knows more about the download than the native backend.
491             return !entry.isAutoResumable;
492         } else {
493             // Only the native downloads backend knows about the download.
494             if (item.getDownloadInfo().state() == DownloadState.IN_PROGRESS) {
495                 return item.getDownloadInfo().isPaused();
496             } else {
497                 return item.getDownloadInfo().state() == DownloadState.INTERRUPTED;
498             }
499         }
500     }
501 
502     /**
503      * Return whether a download is pending.
504      * @param item Download to check the status of.
505      * @return Whether the download is pending or not.
506      */
isDownloadPending(DownloadItem item)507     public static boolean isDownloadPending(DownloadItem item) {
508         DownloadSharedPreferenceHelper helper = DownloadSharedPreferenceHelper.getInstance();
509         DownloadSharedPreferenceEntry entry =
510                 helper.getDownloadSharedPreferenceEntry(item.getContentId());
511         return entry != null && item.getDownloadInfo().state() == DownloadState.INTERRUPTED
512                 && entry.isAutoResumable;
513     }
514 
515     @NativeMethods
516     interface Natives {
getResumeMode(String url, @FailState int failState)517         int getResumeMode(String url, @FailState int failState);
518     }
519 }
520