Commit d20d0c26 authored by John Abd-El-Malek's avatar John Abd-El-Malek Committed by Commit Bot

Add system notification UI for downloads in WebLayer.

This closely matches Android Download Manager's UI with the exception of skipping the "% downloaded" part since it seemed redundant with the progress bar. The embedder can disable the UI if they choose.

The few UI strings need to be translated. The button strings can be reused from Chrome, but we need to figure out what the rest of the text should say and add translations if they're not shared.

Bug: 1025603
Change-Id: I61ba3b821c417de13191a67a85487b61b5f9c86f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2002980Reviewed-by: default avatarBo <boliu@chromium.org>
Reviewed-by: default avatarClark DuVall <cduvall@chromium.org>
Reviewed-by: default avatarDavid Trainor <dtrainor@chromium.org>
Commit-Queue: John Abd-El-Malek <jam@chromium.org>
Cr-Commit-Position: refs/heads/master@{#734589}
parent 2d23c49b
...@@ -77,6 +77,7 @@ public class DownloadCallbackTest { ...@@ -77,6 +77,7 @@ public class DownloadCallbackTest {
@Override @Override
public void onDownloadStarted(Download download) { public void onDownloadStarted(Download download) {
mSeenStarted = true; mSeenStarted = true;
download.disableNotification();
} }
@Override @Override
...@@ -85,6 +86,7 @@ public class DownloadCallbackTest { ...@@ -85,6 +86,7 @@ public class DownloadCallbackTest {
mLocation = download.getLocation().toString(); mLocation = download.getLocation().toString();
mState = download.getState(); mState = download.getState();
mError = download.getError(); mError = download.getError();
mMimetype = download.getMimeType();
} }
@Override @Override
...@@ -213,5 +215,6 @@ public class DownloadCallbackTest { ...@@ -213,5 +215,6 @@ public class DownloadCallbackTest {
"org.chromium.weblayer.shell/cache/weblayer/Downloads/")); "org.chromium.weblayer.shell/cache/weblayer/Downloads/"));
Assert.assertEquals(DownloadState.COMPLETE, mCallback.mState); Assert.assertEquals(DownloadState.COMPLETE, mCallback.mState);
Assert.assertEquals(DownloadError.NO_ERROR, mCallback.mError); Assert.assertEquals(DownloadError.NO_ERROR, mCallback.mError);
Assert.assertEquals("text/html", mCallback.mMimetype);
} }
} }
...@@ -80,6 +80,7 @@ class DownloadBrowserTest : public WebLayerBrowserTest, ...@@ -80,6 +80,7 @@ class DownloadBrowserTest : public WebLayerBrowserTest,
base::FilePath download_location() { return download_location_; } base::FilePath download_location() { return download_location_; }
int64_t total_bytes() { return total_bytes_; } int64_t total_bytes() { return total_bytes_; }
DownloadError download_state() { return download_state_; } DownloadError download_state() { return download_state_; }
std::string mime_type() { return mime_type_; }
int completed_count() { return completed_count_; } int completed_count() { return completed_count_; }
int failed_count() { return failed_count_; } int failed_count() { return failed_count_; }
int download_dropped_count() { return download_dropped_count_; } int download_dropped_count() { return download_dropped_count_; }
...@@ -118,6 +119,7 @@ class DownloadBrowserTest : public WebLayerBrowserTest, ...@@ -118,6 +119,7 @@ class DownloadBrowserTest : public WebLayerBrowserTest,
download_location_ = download->GetLocation(); download_location_ = download->GetLocation();
total_bytes_ = download->GetTotalBytes(); total_bytes_ = download->GetTotalBytes();
download_state_ = download->GetError(); download_state_ = download->GetError();
mime_type_ = download->GetMimeType();
CHECK_EQ(download->GetReceivedBytes(), total_bytes_); CHECK_EQ(download->GetReceivedBytes(), total_bytes_);
CHECK_EQ(download->GetState(), DownloadState::kComplete); CHECK_EQ(download->GetState(), DownloadState::kComplete);
completed_run_loop_->Quit(); completed_run_loop_->Quit();
...@@ -142,6 +144,7 @@ class DownloadBrowserTest : public WebLayerBrowserTest, ...@@ -142,6 +144,7 @@ class DownloadBrowserTest : public WebLayerBrowserTest,
base::FilePath download_location_; base::FilePath download_location_;
int64_t total_bytes_ = 0; int64_t total_bytes_ = 0;
DownloadError download_state_ = DownloadError::kNoError; DownloadError download_state_ = DownloadError::kNoError;
std::string mime_type_;
int completed_count_ = 0; int completed_count_ = 0;
int failed_count_ = 0; int failed_count_ = 0;
int download_dropped_count_ = 0; int download_dropped_count_ = 0;
...@@ -204,6 +207,7 @@ IN_PROC_BROWSER_TEST_F(DownloadBrowserTest, Basic) { ...@@ -204,6 +207,7 @@ IN_PROC_BROWSER_TEST_F(DownloadBrowserTest, Basic) {
EXPECT_EQ(failed_count(), 0); EXPECT_EQ(failed_count(), 0);
EXPECT_EQ(download_dropped_count(), 0); EXPECT_EQ(download_dropped_count(), 0);
EXPECT_EQ(download_state(), DownloadError::kNoError); EXPECT_EQ(download_state(), DownloadError::kNoError);
EXPECT_EQ(mime_type(), "text/html");
// Check that the size on disk matches what's expected. // Check that the size on disk matches what's expected.
{ {
......
...@@ -54,6 +54,13 @@ base::android::ScopedJavaLocalRef<jstring> DownloadImpl::GetLocation( ...@@ -54,6 +54,13 @@ base::android::ScopedJavaLocalRef<jstring> DownloadImpl::GetLocation(
return base::android::ScopedJavaLocalRef<jstring>( return base::android::ScopedJavaLocalRef<jstring>(
base::android::ConvertUTF8ToJavaString(env, GetLocation().value())); base::android::ConvertUTF8ToJavaString(env, GetLocation().value()));
} }
base::android::ScopedJavaLocalRef<jstring> DownloadImpl::GetMimeTypeImpl(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj) {
return base::android::ScopedJavaLocalRef<jstring>(
base::android::ConvertUTF8ToJavaString(env, GetMimeType()));
}
#endif #endif
DownloadState DownloadImpl::GetState() { DownloadState DownloadImpl::GetState() {
...@@ -110,6 +117,10 @@ base::FilePath DownloadImpl::GetLocation() { ...@@ -110,6 +117,10 @@ base::FilePath DownloadImpl::GetLocation() {
return item_->GetTargetFilePath(); return item_->GetTargetFilePath();
} }
std::string DownloadImpl::GetMimeType() {
return item_->GetMimeType();
}
DownloadError DownloadImpl::GetError() { DownloadError DownloadImpl::GetError() {
auto reason = item_->GetLastReason(); auto reason = item_->GetLastReason();
if (reason == download::DOWNLOAD_INTERRUPT_REASON_NONE) if (reason == download::DOWNLOAD_INTERRUPT_REASON_NONE)
......
...@@ -57,6 +57,10 @@ class DownloadImpl : public Download, public base::SupportsUserData::Data { ...@@ -57,6 +57,10 @@ class DownloadImpl : public Download, public base::SupportsUserData::Data {
base::android::ScopedJavaLocalRef<jstring> GetLocation( base::android::ScopedJavaLocalRef<jstring> GetLocation(
JNIEnv* env, JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj); const base::android::JavaParamRef<jobject>& obj);
// Add Impl suffix to avoid compiler clash with the C++ interface method.
base::android::ScopedJavaLocalRef<jstring> GetMimeTypeImpl(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj);
int GetError(JNIEnv* env, const base::android::JavaParamRef<jobject>& obj) { int GetError(JNIEnv* env, const base::android::JavaParamRef<jobject>& obj) {
return static_cast<int>(GetError()); return static_cast<int>(GetError());
} }
...@@ -74,6 +78,7 @@ class DownloadImpl : public Download, public base::SupportsUserData::Data { ...@@ -74,6 +78,7 @@ class DownloadImpl : public Download, public base::SupportsUserData::Data {
void Resume() override; void Resume() override;
void Cancel() override; void Cancel() override;
base::FilePath GetLocation() override; base::FilePath GetLocation() override;
std::string GetMimeType() override;
DownloadError GetError() override; DownloadError GetError() override;
private: private:
......
...@@ -21,10 +21,12 @@ import org.chromium.weblayer_private.interfaces.ObjectWrapper; ...@@ -21,10 +21,12 @@ import org.chromium.weblayer_private.interfaces.ObjectWrapper;
@JNINamespace("weblayer") @JNINamespace("weblayer")
public final class DownloadCallbackProxy { public final class DownloadCallbackProxy {
private long mNativeDownloadCallbackProxy; private long mNativeDownloadCallbackProxy;
private BrowserImpl mBrowser;
private IDownloadCallbackClient mClient; private IDownloadCallbackClient mClient;
DownloadCallbackProxy(long tab, IDownloadCallbackClient client) { DownloadCallbackProxy(BrowserImpl browser, long tab, IDownloadCallbackClient client) {
assert client != null; assert client != null;
mBrowser = browser;
mClient = client; mClient = client;
mNativeDownloadCallbackProxy = mNativeDownloadCallbackProxy =
DownloadCallbackProxyJni.get().createDownloadCallbackProxy(this, tab); DownloadCallbackProxyJni.get().createDownloadCallbackProxy(this, tab);
...@@ -70,27 +72,31 @@ public final class DownloadCallbackProxy { ...@@ -70,27 +72,31 @@ public final class DownloadCallbackProxy {
@CalledByNative @CalledByNative
private DownloadImpl createDownload(long nativeDownloadImpl) { private DownloadImpl createDownload(long nativeDownloadImpl) {
return new DownloadImpl(mClient, nativeDownloadImpl); return new DownloadImpl(mBrowser, mClient, nativeDownloadImpl);
} }
@CalledByNative @CalledByNative
private void downloadStarted(DownloadImpl download) throws RemoteException { private void downloadStarted(DownloadImpl download) throws RemoteException {
mClient.downloadStarted(download.getClientDownload()); mClient.downloadStarted(download.getClientDownload());
download.downloadStarted();
} }
@CalledByNative @CalledByNative
private void downloadProgressChanged(DownloadImpl download) throws RemoteException { private void downloadProgressChanged(DownloadImpl download) throws RemoteException {
mClient.downloadProgressChanged(download.getClientDownload()); mClient.downloadProgressChanged(download.getClientDownload());
download.downloadProgressChanged();
} }
@CalledByNative @CalledByNative
private void downloadCompleted(DownloadImpl download) throws RemoteException { private void downloadCompleted(DownloadImpl download) throws RemoteException {
mClient.downloadCompleted(download.getClientDownload()); mClient.downloadCompleted(download.getClientDownload());
download.downloadCompleted();
} }
@CalledByNative @CalledByNative
private void downloadFailed(DownloadImpl download) throws RemoteException { private void downloadFailed(DownloadImpl download) throws RemoteException {
mClient.downloadFailed(download.getClientDownload()); mClient.downloadFailed(download.getClientDownload());
download.downloadFailed();
} }
@NativeMethods @NativeMethods
......
...@@ -4,8 +4,22 @@ ...@@ -4,8 +4,22 @@
package org.chromium.weblayer_private; package org.chromium.weblayer_private;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.RemoteException; import android.os.RemoteException;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.text.TextUtils;
import org.chromium.base.ContentUriUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.annotations.CalledByNative; import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace; import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods; import org.chromium.base.annotations.NativeMethods;
...@@ -17,16 +31,110 @@ import org.chromium.weblayer_private.interfaces.IDownload; ...@@ -17,16 +31,110 @@ import org.chromium.weblayer_private.interfaces.IDownload;
import org.chromium.weblayer_private.interfaces.IDownloadCallbackClient; import org.chromium.weblayer_private.interfaces.IDownloadCallbackClient;
import org.chromium.weblayer_private.interfaces.StrictModeWorkaround; import org.chromium.weblayer_private.interfaces.StrictModeWorkaround;
import java.io.File;
import java.util.HashMap;
/** /**
* Implementation of IDownload. * Implementation of IDownload.
*/ */
@JNINamespace("weblayer") @JNINamespace("weblayer")
public final class DownloadImpl extends IDownload.Stub { public final class DownloadImpl extends IDownload.Stub {
// These actions have to be synchronized with the receiver defined in AndroidManifest.xml.
static final String OPEN_INTENT = "org.chromium.weblayer.downloads.OPEN";
static final String DELETE_INTENT = "org.chromium.weblayer.downloads.DELETE";
static final String PAUSE_INTENT = "org.chromium.weblayer.downloads.PAUSE";
static final String RESUME_INTENT = "org.chromium.weblayer.downloads.RESUME";
static final String CANCEL_INTENT = "org.chromium.weblayer.downloads.CANCEL";
static final String EXTRA_NOTIFICATION_ID = "org.chromium.weblayer.downloads.NOTIFICATION_ID";
static final String EXTRA_NOTIFICATION_LOCATION =
"org.chromium.weblayer.downloads.NOTIFICATION_LOCATION";
static final String EXTRA_NOTIFICATION_MIME_TYPE =
"org.chromium.weblayer.downloads.NOTIFICATION_MIME_TYPE";
static final String PREF_NEXT_NOTIFICATION_ID =
"org.chromium.weblayer.downloads.notification_next_id";
private static final String CHANNEL_ID = "org.chromium.weblayer.downloads.channel";
private static final String TAG = "DownloadImpl";
private BrowserImpl mBrowser;
private final IDownloadCallbackClient mClient;
private final IClientDownload mClientDownload; private final IClientDownload mClientDownload;
// WARNING: DownloadImpl may outlive the native side, in which case this member is set to 0. // WARNING: DownloadImpl may outlive the native side, in which case this member is set to 0.
private long mNativeDownloadImpl; private long mNativeDownloadImpl;
private boolean mDisableNotification;
private int mNotificationId;
private NotificationCompat.Builder mBuilder;
private static boolean sCreatedChannel = false;
private static final HashMap<Integer, DownloadImpl> sMap = new HashMap<Integer, DownloadImpl>();
public static void forwardIntent(Context context, Intent intent) {
if (intent.getAction().equals(OPEN_INTENT)) {
String location = intent.getStringExtra(EXTRA_NOTIFICATION_LOCATION);
if (TextUtils.isEmpty(location)) {
Log.d(TAG, "Didn't find location for open intent");
return;
}
String mimeType = intent.getStringExtra(EXTRA_NOTIFICATION_MIME_TYPE);
Intent openIntent = new Intent(Intent.ACTION_VIEW);
if (TextUtils.isEmpty(mimeType)) {
openIntent.setData(ContentUriUtils.getContentUriFromFile(new File(location)));
} else {
openIntent.setDataAndType(
ContentUriUtils.getContentUriFromFile(new File(location)), mimeType);
}
openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
openIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
openIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
context.startActivity(openIntent);
} catch (ActivityNotFoundException ex) {
// TODO: show some UI that there were no apps to handle this?
}
return;
}
int id = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
DownloadImpl download = sMap.get(id);
if (download == null) {
Log.d(TAG, "Didn't find download for " + id);
// TODO(jam): handle download resumption after restart
return;
}
public DownloadImpl(IDownloadCallbackClient client, long nativeDownloadImpl) { if (intent.getAction().equals(PAUSE_INTENT)) {
download.pause();
} else if (intent.getAction().equals(RESUME_INTENT)) {
download.resume();
} else if (intent.getAction().equals(CANCEL_INTENT)) {
download.cancel();
} else if (intent.getAction().equals(DELETE_INTENT)) {
sMap.remove(id);
}
}
/**
* Need to return a unique id, even across crashes, to avoid notification intents with
* different data (e.g. notification GUID) getting dup'd.
*/
private static int getNextNotificationId() {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
int nextId = prefs.getInt(PREF_NEXT_NOTIFICATION_ID, -1);
// Reset the counter when it gets close to max value
if (nextId >= Integer.MAX_VALUE - 1) {
nextId = -1;
}
nextId++;
prefs.edit().putInt(PREF_NEXT_NOTIFICATION_ID, nextId).apply();
return nextId;
}
public DownloadImpl(
BrowserImpl browser, IDownloadCallbackClient client, long nativeDownloadImpl) {
mBrowser = browser;
mClient = client;
mNativeDownloadImpl = nativeDownloadImpl; mNativeDownloadImpl = nativeDownloadImpl;
try { try {
mClientDownload = client.createClientDownload(this); mClientDownload = client.createClientDownload(this);
...@@ -110,6 +218,7 @@ public final class DownloadImpl extends IDownload.Stub { ...@@ -110,6 +218,7 @@ public final class DownloadImpl extends IDownload.Stub {
StrictModeWorkaround.apply(); StrictModeWorkaround.apply();
throwIfNativeDestroyed(); throwIfNativeDestroyed();
DownloadImplJni.get().pause(mNativeDownloadImpl, DownloadImpl.this); DownloadImplJni.get().pause(mNativeDownloadImpl, DownloadImpl.this);
updateNotification();
} }
@Override @Override
...@@ -117,6 +226,7 @@ public final class DownloadImpl extends IDownload.Stub { ...@@ -117,6 +226,7 @@ public final class DownloadImpl extends IDownload.Stub {
StrictModeWorkaround.apply(); StrictModeWorkaround.apply();
throwIfNativeDestroyed(); throwIfNativeDestroyed();
DownloadImplJni.get().resume(mNativeDownloadImpl, DownloadImpl.this); DownloadImplJni.get().resume(mNativeDownloadImpl, DownloadImpl.this);
updateNotification();
} }
@Override @Override
...@@ -124,6 +234,7 @@ public final class DownloadImpl extends IDownload.Stub { ...@@ -124,6 +234,7 @@ public final class DownloadImpl extends IDownload.Stub {
StrictModeWorkaround.apply(); StrictModeWorkaround.apply();
throwIfNativeDestroyed(); throwIfNativeDestroyed();
DownloadImplJni.get().cancel(mNativeDownloadImpl, DownloadImpl.this); DownloadImplJni.get().cancel(mNativeDownloadImpl, DownloadImpl.this);
updateNotification();
} }
@Override @Override
...@@ -133,6 +244,13 @@ public final class DownloadImpl extends IDownload.Stub { ...@@ -133,6 +244,13 @@ public final class DownloadImpl extends IDownload.Stub {
return DownloadImplJni.get().getLocation(mNativeDownloadImpl, DownloadImpl.this); return DownloadImplJni.get().getLocation(mNativeDownloadImpl, DownloadImpl.this);
} }
@Override
public String getMimeType() {
StrictModeWorkaround.apply();
throwIfNativeDestroyed();
return DownloadImplJni.get().getMimeTypeImpl(mNativeDownloadImpl, DownloadImpl.this);
}
@Override @Override
@DownloadError @DownloadError
public int getError() { public int getError() {
...@@ -142,15 +260,217 @@ public final class DownloadImpl extends IDownload.Stub { ...@@ -142,15 +260,217 @@ public final class DownloadImpl extends IDownload.Stub {
DownloadImplJni.get().getError(mNativeDownloadImpl, DownloadImpl.this)); DownloadImplJni.get().getError(mNativeDownloadImpl, DownloadImpl.this));
} }
@Override
public void disableNotification() {
StrictModeWorkaround.apply();
throwIfNativeDestroyed();
mDisableNotification = true;
if (mBuilder != null) {
NotificationManagerCompat notificationManager = getNotificationManager();
if (notificationManager != null) {
notificationManager.cancel(mNotificationId);
}
mBuilder = null;
}
}
private void throwIfNativeDestroyed() { private void throwIfNativeDestroyed() {
if (mNativeDownloadImpl == 0) { if (mNativeDownloadImpl == 0) {
throw new IllegalStateException("Using Download after native destroyed"); throw new IllegalStateException("Using Download after native destroyed");
} }
} }
private Intent createIntent() {
// Because the intent is using classes from the implementation's class loader,
// we need to use the constructor which doesn't take the app's context.
try {
return mClient.createIntent();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
public void downloadStarted() {
if (mDisableNotification) return;
// TODO(jam): create a foreground service while the download is running to avoid the process
// being shut down if the user switches apps.
if (!sCreatedChannel) createNotificationChannel();
mNotificationId = getNextNotificationId();
sMap.put(Integer.valueOf(mNotificationId), this);
Intent pauseIntent = createIntent();
pauseIntent.setAction(PAUSE_INTENT);
pauseIntent.putExtra(EXTRA_NOTIFICATION_ID, mNotificationId);
// See PendingIntent's documentation on why we must use a different requestId as we need
// multiple distinct PendingIntents at a time, one for each notification.
PendingIntent pausePendingIntent =
PendingIntent.getBroadcast(mBrowser.getContext(), mNotificationId, pauseIntent, 0);
Intent cancelIntent = createIntent();
cancelIntent.setAction(CANCEL_INTENT);
cancelIntent.putExtra(EXTRA_NOTIFICATION_ID, mNotificationId);
PendingIntent cancelPendingIntent =
PendingIntent.getBroadcast(mBrowser.getContext(), mNotificationId, cancelIntent, 0);
Intent deleteIntent = createIntent();
deleteIntent.setAction(DELETE_INTENT);
deleteIntent.putExtra(EXTRA_NOTIFICATION_ID, mNotificationId);
PendingIntent deletePendingIntent =
PendingIntent.getBroadcast(mBrowser.getContext(), mNotificationId, deleteIntent, 0);
mBuilder = new NotificationCompat.Builder(mBrowser.getContext(), CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setContentTitle((new File(getLocation())).getName())
.setOngoing(true)
.addAction(0 /* no icon */, "Pause", pausePendingIntent)
.addAction(0 /* no icon */, "Cancel", cancelPendingIntent)
.setDeleteIntent(deletePendingIntent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT);
if (getTotalBytes() == -1) {
mBuilder.setProgress(0, 0, true);
} else {
mBuilder.setProgress(100, 0, false);
}
NotificationManagerCompat notificationManager = getNotificationManager();
if (notificationManager != null) {
// mNotificationId is a unique int for each notification that you must define
notificationManager.notify(mNotificationId, mBuilder.build());
}
}
public void downloadProgressChanged() {
if (mBuilder == null) return;
// The filename might not have been available initially.
mBuilder.setContentTitle((new File(getLocation())).getName());
if (getTotalBytes() > 0) {
int progressCurrent = (int) (getReceivedBytes() * 100 / getTotalBytes());
mBuilder.setProgress(100, progressCurrent, false);
NotificationManagerCompat notificationManager = getNotificationManager();
if (notificationManager != null) {
notificationManager.notify(mNotificationId, mBuilder.build());
}
}
}
public void downloadCompleted() {
if (mBuilder == null) return;
updateNotification();
}
public void downloadFailed() {
if (mBuilder == null) return;
updateNotification();
}
private void createNotificationChannel() {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel =
new NotificationChannel(CHANNEL_ID, "Downloads", importance);
NotificationManager notificationManager =
mBrowser.getContext().getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
sCreatedChannel = true;
}
private void updateNotification() {
if (mBuilder == null) return;
NotificationManagerCompat notificationManager = getNotificationManager();
@DownloadState
int state = getState();
if (state == DownloadState.CANCELLED) {
if (notificationManager != null) {
notificationManager.cancel(mNotificationId);
}
mBuilder = null;
return;
}
mBuilder.mActions.clear();
if (state == DownloadState.COMPLETE) {
Intent openIntent = createIntent();
openIntent.setAction(OPEN_INTENT);
openIntent.putExtra(EXTRA_NOTIFICATION_ID, mNotificationId);
openIntent.putExtra(EXTRA_NOTIFICATION_LOCATION, getLocation());
openIntent.putExtra(EXTRA_NOTIFICATION_MIME_TYPE, getMimeType());
PendingIntent openPendingIntent = PendingIntent.getBroadcast(
mBrowser.getContext(), mNotificationId, openIntent, 0);
mBuilder.setProgress(100, 100, false)
.setOngoing(false)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentIntent(openPendingIntent)
.setAutoCancel(true);
} else if (state == DownloadState.FAILED) {
// TODO(jam): make these strings translated
mBuilder.setContentText("Download failed")
.setOngoing(false)
.setSmallIcon(android.R.drawable.stat_sys_download_done);
} else if (state == DownloadState.IN_PROGRESS) {
Intent pauseIntent = createIntent();
pauseIntent.setAction(PAUSE_INTENT);
pauseIntent.putExtra(EXTRA_NOTIFICATION_ID, mNotificationId);
PendingIntent pausePendingIntent = PendingIntent.getBroadcast(
mBrowser.getContext(), mNotificationId, pauseIntent, 0);
mBuilder.addAction(0 /* no icon */, "Pause", pausePendingIntent)
.setSmallIcon(android.R.drawable.stat_sys_download);
} else if (state == DownloadState.PAUSED) {
Intent resumeIntent = createIntent();
resumeIntent.setAction(RESUME_INTENT);
resumeIntent.putExtra(EXTRA_NOTIFICATION_ID, mNotificationId);
PendingIntent resumePendingIntent = PendingIntent.getBroadcast(
mBrowser.getContext(), mNotificationId, resumeIntent, 0);
mBuilder.addAction(0 /* no icon */, "Resume", resumePendingIntent)
.setSmallIcon(android.R.drawable.ic_media_pause);
}
if (state == DownloadState.IN_PROGRESS || state == DownloadState.PAUSED) {
Intent cancelIntent = createIntent();
cancelIntent.setAction(CANCEL_INTENT);
cancelIntent.putExtra(EXTRA_NOTIFICATION_ID, mNotificationId);
PendingIntent cancelPendingIntent = PendingIntent.getBroadcast(
mBrowser.getContext(), mNotificationId, cancelIntent, 0);
mBuilder.addAction(0 /* no icon */, "Cancel", cancelPendingIntent);
}
if (notificationManager != null) {
notificationManager.notify(mNotificationId, mBuilder.build());
}
}
/**
* Returns the notification manager. May return null if there's no context, e.g. when the
* fragment is moving between activies or when the activity is destroyed before the fragment.
*/
private NotificationManagerCompat getNotificationManager() {
if (mBrowser.getContext() == null) {
return null;
}
return NotificationManagerCompat.from(mBrowser.getContext());
}
@CalledByNative @CalledByNative
private void onNativeDestroyed() { private void onNativeDestroyed() {
mNativeDownloadImpl = 0; mNativeDownloadImpl = 0;
sMap.remove(mNotificationId);
// TODO: this should likely notify delegate in some way. // TODO: this should likely notify delegate in some way.
} }
...@@ -164,6 +484,7 @@ public final class DownloadImpl extends IDownload.Stub { ...@@ -164,6 +484,7 @@ public final class DownloadImpl extends IDownload.Stub {
void resume(long nativeDownloadImpl, DownloadImpl caller); void resume(long nativeDownloadImpl, DownloadImpl caller);
void cancel(long nativeDownloadImpl, DownloadImpl caller); void cancel(long nativeDownloadImpl, DownloadImpl caller);
String getLocation(long nativeDownloadImpl, DownloadImpl caller); String getLocation(long nativeDownloadImpl, DownloadImpl caller);
String getMimeTypeImpl(long nativeDownloadImpl, DownloadImpl caller);
int getError(long nativeDownloadImpl, DownloadImpl caller); int getError(long nativeDownloadImpl, DownloadImpl caller);
} }
} }
...@@ -220,7 +220,7 @@ public final class TabImpl extends ITab.Stub { ...@@ -220,7 +220,7 @@ public final class TabImpl extends ITab.Stub {
StrictModeWorkaround.apply(); StrictModeWorkaround.apply();
if (client != null) { if (client != null) {
if (mDownloadCallbackProxy == null) { if (mDownloadCallbackProxy == null) {
mDownloadCallbackProxy = new DownloadCallbackProxy(mNativeTab, client); mDownloadCallbackProxy = new DownloadCallbackProxy(mBrowser, mNativeTab, client);
} else { } else {
mDownloadCallbackProxy.setClient(client); mDownloadCallbackProxy.setClient(client);
} }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package org.chromium.weblayer_private; package org.chromium.weblayer_private;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.AssetManager; import android.content.res.AssetManager;
...@@ -255,6 +256,13 @@ public final class WebLayerImpl extends IWebLayer.Stub { ...@@ -255,6 +256,13 @@ public final class WebLayerImpl extends IWebLayer.Stub {
return CrashReporterControllerImpl.getInstance(); return CrashReporterControllerImpl.getInstance();
} }
@Override
public void onReceivedDownloadNotification(IObjectWrapper appContextWrapper, Intent intent) {
StrictModeWorkaround.apply();
Context context = ObjectWrapper.unwrap(appContextWrapper, Context.class);
DownloadImpl.forwardIntent(context, intent);
}
/** /**
* Creates a remote context. This should only be used for backwards compatibility when the * Creates a remote context. This should only be used for backwards compatibility when the
* client was not sending the remote context. * client was not sending the remote context.
......
...@@ -16,4 +16,6 @@ interface IDownload { ...@@ -16,4 +16,6 @@ interface IDownload {
void cancel() = 5; void cancel() = 5;
String getLocation() = 6; String getLocation() = 6;
int getError() = 7; int getError() = 7;
String getMimeType() = 8;
void disableNotification() = 9;
} }
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
package org.chromium.weblayer_private.interfaces; package org.chromium.weblayer_private.interfaces;
import android.content.Intent;
import org.chromium.weblayer_private.interfaces.IClientDownload; import org.chromium.weblayer_private.interfaces.IClientDownload;
import org.chromium.weblayer_private.interfaces.IDownload; import org.chromium.weblayer_private.interfaces.IDownload;
import org.chromium.weblayer_private.interfaces.IObjectWrapper; import org.chromium.weblayer_private.interfaces.IObjectWrapper;
...@@ -19,4 +20,5 @@ interface IDownloadCallbackClient { ...@@ -19,4 +20,5 @@ interface IDownloadCallbackClient {
void downloadProgressChanged(IClientDownload download) = 4; void downloadProgressChanged(IClientDownload download) = 4;
void downloadCompleted(IClientDownload download) = 5; void downloadCompleted(IClientDownload download) = 5;
void downloadFailed(IClientDownload download) = 6; void downloadFailed(IClientDownload download) = 6;
Intent createIntent() = 7;
} }
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
package org.chromium.weblayer_private.interfaces; package org.chromium.weblayer_private.interfaces;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import org.chromium.weblayer_private.interfaces.IBrowserFragment; import org.chromium.weblayer_private.interfaces.IBrowserFragment;
...@@ -67,4 +68,7 @@ interface IWebLayer { ...@@ -67,4 +68,7 @@ interface IWebLayer {
ICrashReporterController getCrashReporterController( ICrashReporterController getCrashReporterController(
in IObjectWrapper appContext, in IObjectWrapper appContext,
in IObjectWrapper remoteContext) = 10; in IObjectWrapper remoteContext) = 10;
// Forwards download intent notifications to the implementation.
void onReceivedDownloadNotification(in IObjectWrapper appContext, in Intent intent) = 11;
} }
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
#ifndef WEBLAYER_PUBLIC_DOWNLOAD_H_ #ifndef WEBLAYER_PUBLIC_DOWNLOAD_H_
#define WEBLAYER_PUBLIC_DOWNLOAD_H_ #define WEBLAYER_PUBLIC_DOWNLOAD_H_
#include <string>
namespace base { namespace base {
class FilePath; class FilePath;
} }
...@@ -77,6 +79,9 @@ class Download { ...@@ -77,6 +79,9 @@ class Download {
// available until the download completes successfully. // available until the download completes successfully.
virtual base::FilePath GetLocation() = 0; virtual base::FilePath GetLocation() = 0;
// Returns the effective MIME type of downloaded content.
virtual std::string GetMimeType() = 0;
// Return information about the error, if any, that was encountered during the // Return information about the error, if any, that was encountered during the
// download. // download.
virtual DownloadError GetError() = 0; virtual DownloadError GetError() = 0;
......
...@@ -52,5 +52,17 @@ ...@@ -52,5 +52,17 @@
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/weblayer_file_paths" /> android:resource="@xml/weblayer_file_paths" />
</provider> </provider>
<receiver android:name="org.chromium.weblayer.DownloadBroadcastReceiver"
android:exported="false">
<intent-filter>
<!-- these need to be in sync with DownloadImpl.java-->
<action android:name="org.chromium.weblayer.downloads.OPEN"/>
<action android:name="org.chromium.weblayer.downloads.DELETE"/>
<action android:name="org.chromium.weblayer.downloads.PAUSE"/>
<action android:name="org.chromium.weblayer.downloads.RESUME"/>
<action android:name="org.chromium.weblayer.downloads.CANCEL"/>
</intent-filter>
</receiver>
</application> </application>
</manifest> </manifest>
...@@ -33,6 +33,7 @@ android_library("java") { ...@@ -33,6 +33,7 @@ android_library("java") {
"org/chromium/weblayer/CrashReporterCallback.java", "org/chromium/weblayer/CrashReporterCallback.java",
"org/chromium/weblayer/CrashReporterController.java", "org/chromium/weblayer/CrashReporterController.java",
"org/chromium/weblayer/Download.java", "org/chromium/weblayer/Download.java",
"org/chromium/weblayer/DownloadBroadcastReceiver.java",
"org/chromium/weblayer/DownloadCallback.java", "org/chromium/weblayer/DownloadCallback.java",
"org/chromium/weblayer/DownloadError.java", "org/chromium/weblayer/DownloadError.java",
"org/chromium/weblayer/DownloadState.java", "org/chromium/weblayer/DownloadState.java",
......
...@@ -26,6 +26,18 @@ public final class Download extends IClientDownload.Stub { ...@@ -26,6 +26,18 @@ public final class Download extends IClientDownload.Stub {
mDownloadImpl = impl; mDownloadImpl = impl;
} }
/**
* By default downloads will show a system notification. Call this to disable it.
*/
public void disableNotification() {
ThreadCheck.ensureOnUiThread();
try {
mDownloadImpl.disableNotification();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
@DownloadState @DownloadState
public int getState() { public int getState() {
ThreadCheck.ensureOnUiThread(); ThreadCheck.ensureOnUiThread();
...@@ -111,6 +123,19 @@ public final class Download extends IClientDownload.Stub { ...@@ -111,6 +123,19 @@ public final class Download extends IClientDownload.Stub {
} }
} }
/**
* Returns the effective MIME type of downloaded content.
*/
@NonNull
public String getMimeType() {
ThreadCheck.ensureOnUiThread();
try {
return mDownloadImpl.getMimeType();
} catch (RemoteException e) {
throw new APICallException(e);
}
}
/** /**
* Return information about the error, if any, that was encountered during the download. * Return information about the error, if any, that was encountered during the download.
*/ */
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.weblayer;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.RemoteException;
import org.chromium.weblayer_private.interfaces.ObjectWrapper;
/**
* Listens to events from the download system notifications.
*/
public class DownloadBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
try {
WebLayer.loadAsync(context, webLayer -> {
try {
webLayer.getImpl().onReceivedDownloadNotification(
ObjectWrapper.wrap(context), intent);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
});
} catch (UnsupportedVersionException e) {
throw new RuntimeException(e);
}
}
}
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
package org.chromium.weblayer; package org.chromium.weblayer;
import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.RemoteException; import android.os.RemoteException;
import android.webkit.ValueCallback; import android.webkit.ValueCallback;
...@@ -328,6 +329,15 @@ public final class Tab { ...@@ -328,6 +329,15 @@ public final class Tab {
StrictModeWorkaround.apply(); StrictModeWorkaround.apply();
mCallback.onDownloadFailed((Download) download); mCallback.onDownloadFailed((Download) download);
} }
@Override
public Intent createIntent() {
StrictModeWorkaround.apply();
// Intent objects need to be created in the client library so they can refer to the
// broadcast receiver that will handle them. The broadcast receiver needs to be in the
// client library because it's referenced in the manifest.
return new Intent(WebLayer.getAppContext(), DownloadBroadcastReceiver.class);
}
} }
private static final class ErrorPageCallbackClientImpl extends IErrorPageCallbackClient.Stub { private static final class ErrorPageCallbackClientImpl extends IErrorPageCallbackClient.Stub {
......
...@@ -47,6 +47,9 @@ public final class WebLayer { ...@@ -47,6 +47,9 @@ public final class WebLayer {
@Nullable @Nullable
private static Context sRemoteContext; private static Context sRemoteContext;
@Nullable
private static Context sAppContext;
@Nullable @Nullable
private static WebLayerLoader sLoader; private static WebLayerLoader sLoader;
...@@ -123,6 +126,10 @@ public final class WebLayer { ...@@ -123,6 +126,10 @@ public final class WebLayer {
return sLoader; return sLoader;
} }
IWebLayer getImpl() {
return mImpl;
}
/** /**
* Returns the supported version. Using any functions defined in a newer version than * Returns the supported version. Using any functions defined in a newer version than
* returned by {@link getSupportedMajorVersion} result in throwing an * returned by {@link getSupportedMajorVersion} result in throwing an
...@@ -151,6 +158,12 @@ public final class WebLayer { ...@@ -151,6 +158,12 @@ public final class WebLayer {
return sLoader.getMajorVersion(); return sLoader.getMajorVersion();
} }
// Internal getter for the app Context. This should only be used when you know WebLayer has
// been initialized.
static Context getAppContext() {
return sAppContext;
}
/** /**
* Returns the Chrome version of the WebLayer implementation. This will return a full version * Returns the Chrome version of the WebLayer implementation. This will return a full version
* string such as "79.0.3945.0", while {@link getSupportedMajorVersion} will only return the * string such as "79.0.3945.0", while {@link getSupportedMajorVersion} will only return the
...@@ -405,6 +418,7 @@ public final class WebLayer { ...@@ -405,6 +418,7 @@ public final class WebLayer {
} }
Class<?> webViewFactoryClass = Class.forName("android.webkit.WebViewFactory"); Class<?> webViewFactoryClass = Class.forName("android.webkit.WebViewFactory");
String implPackageName = getImplPackageName(appContext); String implPackageName = getImplPackageName(appContext);
sAppContext = appContext;
if (implPackageName != null) { if (implPackageName != null) {
sRemoteContext = createRemoteContextFromPackageName(appContext, implPackageName); sRemoteContext = createRemoteContextFromPackageName(appContext, implPackageName);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
......
...@@ -8,4 +8,5 @@ ...@@ -8,4 +8,5 @@
<paths xmlns:android="http://schemas.android.com/apk/res/android"> <paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="images" path="images/"/> <files-path name="images" path="images/"/>
<external-path name="external_files" path="."/>
</paths> </paths>
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
package org.chromium.weblayer.shell; package org.chromium.weblayer.shell;
import android.app.DownloadManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
...@@ -281,11 +280,7 @@ public class WebLayerShellActivity extends FragmentActivity { ...@@ -281,11 +280,7 @@ public class WebLayerShellActivity extends FragmentActivity {
@Override @Override
public boolean onInterceptDownload(Uri uri, String userAgent, String contentDisposition, public boolean onInterceptDownload(Uri uri, String userAgent, String contentDisposition,
String mimetype, long contentLength) { String mimetype, long contentLength) {
DownloadManager.Request request = new DownloadManager.Request(uri); return false;
request.setNotificationVisibility(
DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
getSystemService(DownloadManager.class).enqueue(request);
return true;
} }
@Override @Override
......
test <html>
\ No newline at end of file <body>
test
</body>
</html>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment