// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/download/android/download_controller.h"
#include <memory>
#include <utility>
#include <vector>
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/json/values_util.h"
#include "base/lazy_instance.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/synchronization/lock.h"
#include "chrome/browser/android/android_theme_resources.h"
#include "chrome/browser/android/profile_key_startup_accessor.h"
#include "chrome/browser/android/profile_key_util.h"
#include "chrome/browser/android/tab_android.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/download/android/dangerous_download_infobar_delegate.h"
#include "chrome/browser/download/android/download_manager_service.h"
#include "chrome/browser/download/android/download_utils.h"
#include "chrome/browser/download/android/new_navigation_observer.h"
#include "chrome/browser/download/download_offline_content_provider.h"
#include "chrome/browser/download/download_offline_content_provider_factory.h"
#include "chrome/browser/download/download_stats.h"
#include "chrome/browser/download/insecure_download_blocking.h"
#include "chrome/browser/flags/android/chrome_feature_list.h"
#include "chrome/browser/offline_pages/android/offline_page_bridge.h"
#include "chrome/browser/permissions/permission_update_message_controller_android.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/branded_strings.h"
#include "components/download/content/public/context_menu_download.h"
#include "components/download/public/common/android/auto_resumption_handler.h"
#include "components/download/public/common/download_item.h"
#include "components/infobars/content/content_infobar_manager.h"
#include "components/pdf/common/constants.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/safe_browsing/android/safe_browsing_api_handler_bridge.h"
#include "components/safe_browsing/core/browser/db/database_manager.h"
#include "components/safe_browsing/core/browser/db/v4_protocol_manager_util.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/download_manager_delegate.h"
#include "content/public/browser/download_request_utils.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/common/content_features.h"
#include "net/base/filename_util.h"
#include "ui/android/view_android.h"
#include "ui/android/window_android.h"
#include "ui/base/device_form_factor.h"
#include "ui/base/page_transition_types.h"
#include "url/android/gurl_android.h"
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/android/chrome_jni_headers/DownloadController_jni.h"
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;
using content::BrowserContext;
using content::BrowserThread;
using content::ContextMenuParams;
using content::DownloadManager;
using content::WebContents;
using download::DownloadItem;
namespace {
// Guards download_controller_
base::LazyInstance<base::Lock>::DestructorAtExit g_download_controller_lock_;
void CreateContextMenuDownloadInternal(
const content::WebContents::Getter& wc_getter,
const content::ContextMenuParams& params,
bool is_link,
bool granted) {
content::WebContents* web_contents = wc_getter.Run();
if (!granted)
return;
if (!web_contents) {
return;
}
RecordDownloadSource(DOWNLOAD_INITIATED_BY_CONTEXT_MENU);
auto origin = offline_pages::android::OfflinePageBridge::GetEncodedOriginApp(
web_contents);
download::CreateContextMenuDownload(web_contents, params, origin, is_link);
}
// Helper class for retrieving a DownloadManager.
class DownloadManagerGetter : public DownloadManager::Observer {
public:
explicit DownloadManagerGetter(DownloadManager* manager) : manager_(manager) {
manager_->AddObserver(this);
}
DownloadManagerGetter(const DownloadManagerGetter&) = delete;
DownloadManagerGetter& operator=(const DownloadManagerGetter&) = delete;
~DownloadManagerGetter() override {
if (manager_)
manager_->RemoveObserver(this);
}
void ManagerGoingDown(DownloadManager* manager) override {
manager_ = nullptr;
}
DownloadManager* manager() { return manager_; }
private:
raw_ptr<DownloadManager> manager_;
};
void RemoveDownloadItem(std::unique_ptr<DownloadManagerGetter> getter,
const std::string& guid) {
if (!getter->manager())
return;
DownloadItem* item = getter->manager()->GetDownloadByGuid(guid);
if (item)
item->Remove();
}
void OnRequestFileAccessResult(
const content::WebContents::Getter& web_contents_getter,
DownloadControllerBase::AcquireFileAccessPermissionCallback cb,
bool granted,
const std::string& permission_to_update) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!granted && !permission_to_update.empty() && web_contents_getter.Run()) {
WebContents* web_contents = web_contents_getter.Run();
std::vector<std::string> permissions;
permissions.push_back(permission_to_update);
PermissionUpdateMessageController::CreateForWebContents(web_contents);
PermissionUpdateMessageController::FromWebContents(web_contents)
->ShowMessage(permissions, IDR_ANDORID_MESSAGE_PERMISSION_STORAGE,
IDS_MESSAGE_MISSING_STORAGE_ACCESS_PERMISSION_TITLE,
IDS_MESSAGE_STORAGE_ACCESS_PERMISSION_TEXT,
std::move(cb));
return;
}
std::move(cb).Run(granted);
}
void OnStoragePermissionDecided(
DownloadControllerBase::AcquireFileAccessPermissionCallback cb,
bool granted) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
std::move(cb).Run(granted);
}
bool ShouldOpenPdfInline(DownloadItem* item) {
BrowserContext* context = content::DownloadItemUtils::GetBrowserContext(item);
return context && context->GetDownloadManagerDelegate() &&
context->GetDownloadManagerDelegate()->ShouldOpenPdfInline() &&
!item->IsMustDownload() && item->IsTransient();
}
class DownloadBlocklistChecker
: public safe_browsing::SafeBrowsingDatabaseManager::Client,
public base::RefCounted<DownloadBlocklistChecker> {
public:
explicit DownloadBlocklistChecker(download::DownloadItem* item)
: url_chain_(item->GetUrlChain()) {}
void Start() {
scoped_refptr<safe_browsing::SafeBrowsingDatabaseManager> database_manager;
if (g_browser_process->safe_browsing_service()) {
database_manager =
g_browser_process->safe_browsing_service()->database_manager();
}
if (!database_manager ||
database_manager->CheckDownloadUrl(url_chain_, this)) {
Log(safe_browsing::SBThreatType::SB_THREAT_TYPE_SAFE);
} else {
// Add a reference to this object to prevent it from being destroyed
// before url checking result is returned.
AddRef();
}
}
private:
friend class base::RefCounted<DownloadBlocklistChecker>;
~DownloadBlocklistChecker() override = default;
void Log(safe_browsing::SBThreatType threat_type) {
base::UmaHistogramEnumeration(
"SafeBrowsing.AndroidTelemetry.DownloadUrlChainThreatType",
threat_type);
}
// SafeBrowsingDatabaseManager::Client:
void OnCheckDownloadUrlResult(
const std::vector<GURL>& url_chain,
safe_browsing::SBThreatType threat_type) override {
Log(threat_type);
Release(); // Balanced by AddRef in Start.
}
std::vector<GURL> url_chain_;
};
void RecordDownloadBlocklistState(download::DownloadItem* item) {
// Startup in Chrome minimal mode may start a download before
// initializing the UI thread.
if (!content::BrowserThread::IsThreadInitialized(
content::BrowserThread::UI)) {
return;
}
auto checker = base::MakeRefCounted<DownloadBlocklistChecker>(item);
checker->Start();
}
void CleanupAppVerificationTimestamps(download::DownloadItem* item) {
Profile* profile = Profile::FromBrowserContext(
content::DownloadItemUtils::GetBrowserContext(item));
if (!profile || !profile->GetPrefs()) {
return;
}
ScopedListPrefUpdate update(profile->GetPrefs(),
prefs::kDownloadAppVerificationPromptTimestamps);
update->EraseIf([](const base::Value& timestamp) {
constexpr base::TimeDelta kImpressionWindow = base::Days(90);
std::optional<base::Time> parsed_timestamp = base::ValueToTime(timestamp);
if (!parsed_timestamp.has_value()) {
return true;
}
return base::Time::Now() - parsed_timestamp.value() > kImpressionWindow;
});
}
bool HasSeenTooManyAppVerificationPrompts(download::DownloadItem* item) {
constexpr size_t kMaxImpressions = 3;
Profile* profile = Profile::FromBrowserContext(
content::DownloadItemUtils::GetBrowserContext(item));
if (!profile || !profile->GetPrefs()) {
return false;
}
return profile->GetPrefs()
->GetList(prefs::kDownloadAppVerificationPromptTimestamps)
.size() >= kMaxImpressions;
}
void LogAppVerificationPromptToPrefs(download::DownloadItem* item) {
Profile* profile = Profile::FromBrowserContext(
content::DownloadItemUtils::GetBrowserContext(item));
if (!profile || !profile->GetPrefs()) {
return;
}
ScopedListPrefUpdate update(profile->GetPrefs(),
prefs::kDownloadAppVerificationPromptTimestamps);
update->Append(base::TimeToValue(base::Time::Now()));
}
} // namespace
static void JNI_DownloadController_OnAcquirePermissionResult(
JNIEnv* env,
jlong callback_id,
jboolean granted,
std::string& permission_to_update) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(callback_id);
if (!DownloadController::GetInstance()
->validator()
->ValidateAndClearJavaCallback(callback_id)) {
return;
}
// Convert java long long int to c++ pointer, take ownership.
std::unique_ptr<DownloadController::AcquirePermissionCallback> cb(
reinterpret_cast<DownloadController::AcquirePermissionCallback*>(
callback_id));
std::move(*cb).Run(granted, permission_to_update);
}
static void JNI_DownloadController_CancelDownload(JNIEnv* env,
Profile* profile,
std::string& download_guid) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DownloadManager* download_manager = profile->GetDownloadManager();
if (download_manager) {
DownloadItem* download = download_manager->GetDownloadByGuid(download_guid);
if (download) {
download->Cancel(/*user_cancel=*/false);
}
}
}
static void JNI_DownloadController_DownloadUrl(
JNIEnv* env,
std::string& url,
const base::android::JavaParamRef<jobject>& jweb_contents) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
content::WebContents* web_contents =
content::WebContents::FromJavaWebContents(jweb_contents);
if (!web_contents) {
return;
}
DownloadManager* download_manager =
web_contents->GetBrowserContext()->GetDownloadManager();
if (download_manager) {
std::unique_ptr<download::DownloadUrlParameters> dl_params =
content::DownloadRequestUtils::CreateDownloadForWebContentsMainFrame(
web_contents, GURL(url),
TRAFFIC_ANNOTATION_WITHOUT_PROTO("Download via toolbar menu"));
dl_params->set_content_initiated(false);
dl_params->set_download_source(download::DownloadSource::TOOLBAR_MENU);
download_manager->DownloadUrl(std::move(dl_params));
}
}
// static
DownloadControllerBase* DownloadControllerBase::Get() {
base::AutoLock lock(g_download_controller_lock_.Get());
if (!DownloadControllerBase::download_controller_)
download_controller_ = DownloadController::GetInstance();
return DownloadControllerBase::download_controller_;
}
// static
void DownloadControllerBase::SetDownloadControllerBase(
DownloadControllerBase* download_controller) {
base::AutoLock lock(g_download_controller_lock_.Get());
DownloadControllerBase::download_controller_ = download_controller;
}
// static
void DownloadController::CloseTabIfEmpty(content::WebContents* web_contents,
download::DownloadItem* download) {
if (!web_contents || !web_contents->GetController().IsInitialNavigation())
return;
// If the download is dangerous, don't close the tab now. The dangerous
// infobar needs to be shown.
if (download && download->IsDangerous() &&
(download->GetState() != DownloadItem::CANCELLED)) {
return;
}
TabModel* tab_model = TabModelList::GetTabModelForWebContents(web_contents);
if (!tab_model || tab_model->GetTabCount() == 1)
return;
if (!download) {
web_contents->Close();
return;
}
if (ShouldOpenPdfInline(download) &&
base::EqualsCaseInsensitiveASCII(download->GetMimeType(),
pdf::kPDFMimeType)) {
return;
}
if (download->IsFromExternalApp()) {
DownloadManagerService::GetInstance()->OpenDownloadsPage(
Profile::FromBrowserContext(web_contents->GetBrowserContext()),
DownloadOpenSource::kExternalApp);
// For tablet, download home is opened in the current tab, so don't close
// it.
if (ui::GetDeviceFormFactor() ==
ui::DeviceFormFactor::DEVICE_FORM_FACTOR_TABLET) {
return;
}
}
web_contents->Close();
}
// static
DownloadController* DownloadController::GetInstance() {
return base::Singleton<DownloadController>::get();
}
DownloadController::DownloadController() = default;
DownloadController::~DownloadController() = default;
void DownloadController::AcquireFileAccessPermission(
const content::WebContents::Getter& web_contents_getter,
DownloadControllerBase::AcquireFileAccessPermissionCallback cb) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
WebContents* web_contents = web_contents_getter.Run();
ui::ViewAndroid* view_android =
web_contents ? web_contents->GetNativeView() : nullptr;
ui::WindowAndroid* window_android =
view_android ? view_android->GetWindowAndroid() : nullptr;
ScopedJavaLocalRef<jobject> jwindow_android =
window_android ? window_android->GetJavaObject()
: ScopedJavaLocalRef<jobject>();
JNIEnv* env = base::android::AttachCurrentThread();
bool has_file_access_permission =
Java_DownloadController_hasFileAccess(env, jwindow_android);
if (has_file_access_permission) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(std::move(cb), true));
return;
}
AcquirePermissionCallback callback(base::BindOnce(
&OnRequestFileAccessResult, web_contents_getter,
base::BindOnce(&OnStoragePermissionDecided, std::move(cb))));
// Make copy on the heap so we can pass the pointer through JNI.
intptr_t callback_id = reinterpret_cast<intptr_t>(
new AcquirePermissionCallback(std::move(callback)));
validator_.AddJavaCallback(callback_id);
Java_DownloadController_requestFileAccess(env, callback_id, jwindow_android);
}
void DownloadController::CreateAndroidDownload(
const content::WebContents::Getter& wc_getter,
const DownloadInfo& info) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&DownloadController::StartAndroidDownload,
base::Unretained(this), wc_getter, info));
}
void DownloadController::StartAndroidDownload(
const content::WebContents::Getter& wc_getter,
const DownloadInfo& info) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
AcquireFileAccessPermission(
wc_getter,
base::BindOnce(&DownloadController::StartAndroidDownloadInternal,
base::Unretained(this), wc_getter, info));
}
void DownloadController::StartAndroidDownloadInternal(
const content::WebContents::Getter& wc_getter,
const DownloadInfo& info,
bool allowed) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!allowed)
return;
JNIEnv* env = base::android::AttachCurrentThread();
std::u16string file_name =
net::GetSuggestedFilename(info.url, info.content_disposition,
std::string(), // referrer_charset
std::string(), // suggested_name
info.original_mime_type, default_file_name_);
ScopedJavaLocalRef<jobject> jurl =
url::GURLAndroid::FromNativeGURL(env, info.url);
ScopedJavaLocalRef<jstring> juser_agent =
ConvertUTF8ToJavaString(env, info.user_agent);
ScopedJavaLocalRef<jstring> jmime_type =
ConvertUTF8ToJavaString(env, info.original_mime_type);
ScopedJavaLocalRef<jstring> jcookie =
ConvertUTF8ToJavaString(env, info.cookie);
ScopedJavaLocalRef<jobject> jreferer =
url::GURLAndroid::FromNativeGURL(env, info.referer);
ScopedJavaLocalRef<jstring> jfile_name =
base::android::ConvertUTF16ToJavaString(env, file_name);
Java_DownloadController_enqueueAndroidDownloadManagerRequest(
env, jurl, juser_agent, jfile_name, jmime_type, jcookie, jreferer);
WebContents* web_contents = wc_getter.Run();
CloseTabIfEmpty(web_contents, nullptr);
}
void DownloadController::OnDownloadStarted(DownloadItem* download_item) {
RecordDownloadBlocklistState(download_item);
// For dangerous downloads, we need to show the dangerous infobar before the
// download can start.
if (!download_item->IsDangerous() &&
download_item->GetMimeType() == pdf::kPDFMimeType &&
ShouldOpenPdfInline(download_item)) {
content::WebContents* web_contents =
content::DownloadItemUtils::GetWebContents(download_item);
bool should_cancel_download = true;
if (web_contents &&
!NewNavigationObserver::GetInstance()->HasNewNavigation(web_contents)) {
TabAndroid* tab = TabAndroid::FromWebContents(web_contents);
if (tab) {
JNIEnv* env = base::android::AttachCurrentThread();
ScopedJavaLocalRef<jobject> j_item =
DownloadManagerService::CreateJavaDownloadInfo(env, download_item);
Java_DownloadController_onPdfDownloadStarted(env, tab->GetJavaObject(),
j_item);
should_cancel_download = false;
}
}
NewNavigationObserver::GetInstance()->StopObserving(web_contents);
if (should_cancel_download) {
download_item->Cancel(/*user_cancel=*/false);
return;
}
}
// Register for updates to the DownloadItem.
download_item->RemoveObserver(this);
download_item->AddObserver(this);
if (download::AutoResumptionHandler::Get())
download::AutoResumptionHandler::Get()->OnDownloadStarted(download_item);
ProfileKey* profile_key = GetProfileKey(download_item);
if (!profile_key)
return;
DownloadOfflineContentProviderFactory::GetForKey(profile_key)
->OnDownloadStarted(download_item);
OnDownloadUpdated(download_item);
}
void DownloadController::OnDownloadUpdated(DownloadItem* item) {
if (item->IsTemporary() || item->IsTransient()) {
// Only allow inline pdf file to proceed.
if (item->GetMimeType() != pdf::kPDFMimeType ||
!ShouldOpenPdfInline(item)) {
return;
}
}
if (item->IsDangerous() && (item->GetState() != DownloadItem::CANCELLED)) {
// Dont't show notification for a dangerous download, as user can resume
// the download after browser crash through notification.
OnDangerousDownload(item);
return;
}
if (item->GetState() == DownloadItem::COMPLETE) {
if (ShouldShowAppVerificationPrompt(item)) {
LogAppVerificationPromptToPrefs(item);
app_verification_prompt_download_ = item;
safe_browsing::SafeBrowsingApiHandlerBridge::GetInstance()
.StartEnableVerifyApps(base::BindOnce(
&DownloadController::EnableVerifyAppsDone,
// base::Unretained is safe because `this` is a singleton.
base::Unretained(this), item));
} else if (app_verification_prompt_download_ != item) {
OnDownloadComplete(item);
}
}
}
void DownloadController::OnDownloadDestroyed(download::DownloadItem* item) {
item->RemoveObserver(this);
if (app_verification_prompt_download_ == item) {
app_verification_prompt_download_ = nullptr;
}
}
void DownloadController::OnDangerousDownload(download::DownloadItem* item) {
WebContents* web_contents = content::DownloadItemUtils::GetWebContents(item);
if (!web_contents) {
auto download_manager_getter = std::make_unique<DownloadManagerGetter>(
content::DownloadItemUtils::GetBrowserContext(item)
->GetDownloadManager());
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&RemoveDownloadItem, std::move(download_manager_getter),
item->GetGuid()));
item->RemoveObserver(this);
return;
}
ui::ViewAndroid* view_android =
web_contents ? web_contents->GetNativeView() : nullptr;
ui::WindowAndroid* window_android =
view_android ? view_android->GetWindowAndroid() : nullptr;
if (!dangerous_download_bridge_) {
dangerous_download_bridge_ =
std::make_unique<DangerousDownloadDialogBridge>();
}
dangerous_download_bridge_->Show(item, window_android);
}
void DownloadController::EnableVerifyAppsDone(
download::DownloadItem* item,
safe_browsing::VerifyAppsEnabledResult result) {
base::UmaHistogramEnumeration(
"SBClientDownload.AndroidAppVerificationPromptResult", result);
if (app_verification_prompt_download_ != nullptr) {
app_verification_prompt_download_ = nullptr;
OnDownloadComplete(item);
}
}
void DownloadController::OnDownloadComplete(download::DownloadItem* item) {
JNIEnv* env = base::android::AttachCurrentThread();
ScopedJavaLocalRef<jobject> j_item =
DownloadManagerService::CreateJavaDownloadInfo(env, item);
// Multiple OnDownloadUpdated() notifications may be issued while the
// download is in the COMPLETE state. Only handle one.
item->RemoveObserver(this);
bool is_download_safe = true;
// Call onDownloadCompleted
TabAndroid* tab = nullptr;
if (base::FeatureList::IsEnabled(features::kAndroidOpenPdfInline)) {
// Primary page of the WebContents have changed when showing the native
// page, need to call GetOriginalWebContents() instead.
content::WebContents* web_contents =
content::DownloadItemUtils::GetOriginalWebContents(item);
if (web_contents) {
tab = TabAndroid::FromWebContents(web_contents);
}
download::DownloadItem::InsecureDownloadStatus status =
GetInsecureDownloadStatusForDownload(
Profile::FromBrowserContext(
content::DownloadItemUtils::GetBrowserContext(item)),
item->GetTargetFilePath(), item);
is_download_safe =
(status == download::DownloadItem::InsecureDownloadStatus::SAFE ||
status == download::DownloadItem::InsecureDownloadStatus::VALIDATED);
}
Java_DownloadController_onDownloadCompleted(
env, tab ? tab->GetJavaObject() : nullptr, j_item, is_download_safe);
}
void DownloadController::StartContextMenuDownload(
const ContextMenuParams& params,
WebContents* web_contents,
bool is_link) {
int process_id = web_contents->GetRenderViewHost()->GetProcess()->GetID();
int routing_id = web_contents->GetRenderViewHost()->GetRoutingID();
const content::WebContents::Getter& wc_getter(
base::BindRepeating(&GetWebContents, process_id, routing_id));
AcquireFileAccessPermission(
wc_getter, base::BindOnce(&CreateContextMenuDownloadInternal, wc_getter,
params, is_link));
}
ProfileKey* DownloadController::GetProfileKey(DownloadItem* download_item) {
Profile* profile = Profile::FromBrowserContext(
content::DownloadItemUtils::GetBrowserContext(download_item));
ProfileKey* profile_key;
if (profile)
profile_key = profile->GetProfileKey();
else
profile_key = ProfileKeyStartupAccessor::GetInstance()->profile_key();
return profile_key;
}
bool DownloadController::ShouldShowAppVerificationPrompt(
download::DownloadItem* item) {
if (!base::FeatureList::IsEnabled(safe_browsing::kGooglePlayProtectPrompt)) {
return false;
}
if (item->GetDangerType() != download::DOWNLOAD_DANGER_TYPE_USER_VALIDATED) {
return false;
}
if (app_verification_prompt_download_ != nullptr) {
return false;
}
CleanupAppVerificationTimestamps(item);
if (HasSeenTooManyAppVerificationPrompts(item)) {
return false;
}
return true;
}