diff --git a/app/metrics.yaml b/app/metrics.yaml index cbc50540b..2ac059b63 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -102,6 +102,7 @@ tabs: - received - pre_existing - browser + - downloads bugs: - https://github.com/MozillaReality/FirefoxReality/issues/1609 data_reviews: diff --git a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java index cda1a31c3..4b945a95c 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserActivity.java @@ -60,6 +60,7 @@ import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.OffscreenDisplay; import org.mozilla.vrbrowser.ui.adapters.Language; +import org.mozilla.vrbrowser.ui.widgets.AppServicesProvider; import org.mozilla.vrbrowser.ui.widgets.KeyboardWidget; import org.mozilla.vrbrowser.ui.widgets.NavigationBarWidget; import org.mozilla.vrbrowser.ui.widgets.RootWidget; @@ -303,6 +304,8 @@ protected void onCreate(Bundle savedInstanceState) { checkForCrash(); mLifeCycle.setCurrentState(Lifecycle.State.CREATED); + + getServicesProvider().getDownloadsManager().init(); } protected void initializeWidgets() { @@ -487,6 +490,8 @@ protected void onDestroy() { SessionStore.get().onDestroy(); + getServicesProvider().getDownloadsManager().end(); + super.onDestroy(); mLifeCycle.setCurrentState(Lifecycle.State.DESTROYED); mViewModelStore.clear(); @@ -1592,6 +1597,12 @@ public void updateLocale(@NonNull Context context) { onConfigurationChanged(context.getResources().getConfiguration()); } + @Override + @NonNull + public AppServicesProvider getServicesProvider() { + return (AppServicesProvider)getApplication(); + } + private native void addWidgetNative(int aHandle, WidgetPlacement aPlacement); private native void updateWidgetNative(int aHandle, WidgetPlacement aPlacement); private native void updateVisibleWidgetsNative(); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java index 93aa327a9..c6bc6f089 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/VRBrowserApplication.java @@ -14,19 +14,22 @@ import org.mozilla.vrbrowser.browser.Services; import org.mozilla.vrbrowser.db.AppDatabase; import org.mozilla.vrbrowser.db.DataRepository; +import org.mozilla.vrbrowser.downloads.DownloadsManager; import org.mozilla.vrbrowser.telemetry.GleanMetricsService; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.adapters.Language; +import org.mozilla.vrbrowser.ui.widgets.AppServicesProvider; import org.mozilla.vrbrowser.utils.BitmapCache; import org.mozilla.vrbrowser.utils.LocaleUtils; -public class VRBrowserApplication extends Application { +public class VRBrowserApplication extends Application implements AppServicesProvider { private AppExecutors mAppExecutors; private BitmapCache mBitmapCache; private Services mServices; private Places mPlaces; private Accounts mAccounts; + private DownloadsManager mDownloadsManager; @Override public void onCreate() { @@ -42,6 +45,7 @@ protected void onActivityCreate() { mPlaces = new Places(this); mServices = new Services(this, mPlaces); mAccounts = new Accounts(this); + mDownloadsManager = new DownloadsManager(this); } @Override @@ -67,7 +71,7 @@ public Places getPlaces() { return mPlaces; } - private AppDatabase getDatabase() { + public AppDatabase getDatabase() { return AppDatabase.getAppDatabase(this, mAppExecutors); } @@ -86,4 +90,8 @@ public BitmapCache getBitmapCache() { public Accounts getAccounts() { return mAccounts; } + + public DownloadsManager getDownloadsManager() { + return mDownloadsManager; + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java index 10f99d502..bc30b4a19 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/SettingsStore.java @@ -8,6 +8,7 @@ import android.preference.PreferenceManager; import android.util.Log; +import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.databinding.ObservableBoolean; import androidx.lifecycle.Observer; @@ -23,6 +24,7 @@ import org.mozilla.vrbrowser.telemetry.GleanMetricsService; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; import org.mozilla.vrbrowser.ui.viewmodel.SettingsViewModel; +import org.mozilla.vrbrowser.ui.widgets.menus.library.SortingContextMenuWidget; import org.mozilla.vrbrowser.utils.DeviceType; import org.mozilla.vrbrowser.utils.StringUtils; import org.mozilla.vrbrowser.utils.SystemUtils; @@ -49,6 +51,11 @@ SettingsStore getInstance(final @NonNull Context aContext) { return mSettingsInstance; } + @IntDef(value = { INTERNAL, EXTERNAL}) + public @interface Storage {} + public static final int INTERNAL = 0; + public static final int EXTERNAL = 1; + private Context mContext; private SharedPreferences mPrefs; private SettingsViewModel mSettingsViewModel; @@ -93,6 +100,8 @@ SettingsStore getInstance(final @NonNull Context aContext) { public final static boolean RESTORE_TABS_ENABLED = true; public final static boolean BYPASS_CACHE_ON_RELOAD = false; public final static boolean MULTI_E10S = false; + public final static int DOWNLOADS_STORAGE_DEFAULT = INTERNAL; + public final static int DOWNLOADS_SORTING_ORDER_DEFAULT = SortingContextMenuWidget.SORT_FILENAME_AZ; // Enable telemetry by default (opt-out). public final static boolean CRASH_REPORTING_DEFAULT = false; @@ -719,5 +728,25 @@ public void setMultiE10s(boolean isEnabled) { public boolean isMultiE10s() { return mPrefs.getBoolean(mContext.getString(R.string.settings_key_multi_e10s), MULTI_E10S); } + + public void setDownloadsStorage(@Storage int storage) { + SharedPreferences.Editor editor = mPrefs.edit(); + editor.putInt(mContext.getString(R.string.settings_key_downloads_external), storage); + editor.commit(); + } + + public @Storage int getDownloadsStorage() { + return mPrefs.getInt(mContext.getString(R.string.settings_key_downloads_external), DOWNLOADS_STORAGE_DEFAULT); + } + + public void setDownloadsSortingOrder(@SortingContextMenuWidget.Order int order) { + SharedPreferences.Editor editor = mPrefs.edit(); + editor.putInt(mContext.getString(R.string.settings_key_downloads_sorting_order), order); + editor.commit(); + } + + public @Storage int getDownloadsSortingOrder() { + return mPrefs.getInt(mContext.getString(R.string.settings_key_downloads_sorting_order), DOWNLOADS_SORTING_ORDER_DEFAULT); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java index ccac32ba0..1cb336b87 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/browser/engine/Session.java @@ -1201,6 +1201,13 @@ public GeckoResult onSlowScript(@NonNull GeckoSession aSessi return null; } + @Override + public void onExternalResponse(@NonNull GeckoSession geckoSession, @NonNull GeckoSession.WebResponseInfo webResponseInfo) { + for (GeckoSession.ContentDelegate listener : mContentListeners) { + listener.onExternalResponse(geckoSession, webResponseInfo); + } + } + // TextInput Delegate @Override diff --git a/app/src/common/shared/org/mozilla/vrbrowser/db/Download.java b/app/src/common/shared/org/mozilla/vrbrowser/db/Download.java new file mode 100644 index 000000000..43006a8f5 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/db/Download.java @@ -0,0 +1,4 @@ +package org.mozilla.vrbrowser.db; + +public class Download { +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/db/DownloadDao.java b/app/src/common/shared/org/mozilla/vrbrowser/db/DownloadDao.java new file mode 100644 index 000000000..999e01886 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/db/DownloadDao.java @@ -0,0 +1,4 @@ +package org.mozilla.vrbrowser.db; + +public class DownloadDao { +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/downloads/Download.java b/app/src/common/shared/org/mozilla/vrbrowser/downloads/Download.java new file mode 100644 index 000000000..fb8321de7 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/downloads/Download.java @@ -0,0 +1,182 @@ +package org.mozilla.vrbrowser.downloads; + +import android.app.DownloadManager; +import android.content.Context; +import android.database.Cursor; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; + +import org.mozilla.vrbrowser.ui.adapters.Language; +import org.mozilla.vrbrowser.utils.LocaleUtils; + +import java.io.File; +import java.net.URL; + +public class Download { + + @IntDef(value = { UNAVAILABLE, PENDING, RUNNING, PAUSED, SUCCESSFUL, FAILED}) + @interface Status {} + public static final int UNAVAILABLE = 0; + public static final int PENDING = DownloadManager.STATUS_PENDING; + public static final int RUNNING = DownloadManager.STATUS_RUNNING; + public static final int PAUSED = DownloadManager.STATUS_PAUSED; + public static final int SUCCESSFUL = DownloadManager.STATUS_SUCCESSFUL; + public static final int FAILED = DownloadManager.STATUS_FAILED; + + private static final long MEGABYTE = 1024L * 1024L; + private static final long KILOBYTE = 1024L; + + private long mId; + private String mUri; + private String mMediaType; + private long mSizeBytes; + private long mDownloadedBytes; + private String mOutputFile; + private String mTitle; + private String mDescription; + private @Status int mStatus; + private long mLastModified; + + public static Download from(Cursor cursor) { + Download download = new Download(); + download.mId = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_ID)); + download.mUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_URI)); + int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)); + switch (status) { + case DownloadManager.STATUS_RUNNING: + download.mStatus = RUNNING; + break; + case DownloadManager.STATUS_FAILED: + download.mStatus = FAILED; + break; + case DownloadManager.STATUS_PAUSED: + download.mStatus = PAUSED; + break; + case DownloadManager.STATUS_PENDING: + download.mStatus = PENDING; + break; + case DownloadManager.STATUS_SUCCESSFUL: + download.mStatus = SUCCESSFUL; + break; + default: + download.mStatus = UNAVAILABLE; + } + download.mMediaType = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE)); + download.mTitle = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TITLE)); + download.mOutputFile = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)); + download.mDescription = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION)); + download.mSizeBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)); + download.mDownloadedBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)); + download.mLastModified = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP)); + return download; + } + + public long getId() { + return mId; + } + + public String getUri() { + return mUri; + } + + public String getMediaType() { + return mMediaType; + } + + public long getSizeBytes() { + return mSizeBytes; + } + + public long getDownloadedBytes() { + return mDownloadedBytes; + } + + public String getOutputFile() { + return mOutputFile; + } + + public String getTitle() { + return mTitle; + } + + public String getDescription() { + return mDescription; + } + + public int getStatus() { + return mStatus; + } + + public String getStatusString() { + switch (mStatus) { + case RUNNING: + return "RUNNING"; + case FAILED: + return "FAILED"; + case PAUSED: + return "PAUSED"; + case PENDING: + return "PENDING"; + case SUCCESSFUL: + return "SUCCESSFUL"; + case UNAVAILABLE: + return "UNAVAILABLE"; + default: + return "UNKNOWN"; + } + } + + public long getLastModified() { + return mLastModified; + } + + public double getProgress() { + if (mSizeBytes != -1) { + return mDownloadedBytes*100.0/mSizeBytes; + } + return 0; + } + + public String getFilename() { + try { + File f = new File(new URL(mOutputFile).getPath()); + return f.getName(); + + } catch (Exception e) { + if (mOutputFile != null) { + return mOutputFile; + + } else { + return ""; + } + } + } + + @NonNull + public static String progressString(@NonNull Context context, @NonNull Download download) { + Language language = LocaleUtils.getDisplayLanguage(context); + if (download.mStatus == RUNNING) { + if (download.mSizeBytes < MEGABYTE) { + return String.format(language.getLocale(), "%.2f/%.2fKb (%d%%)", + ((double)download.mDownloadedBytes / (double)KILOBYTE), + ((double)download.mSizeBytes / (double)KILOBYTE), + (download.mDownloadedBytes*100)/download.mSizeBytes); + + } else { + return String.format(language.getLocale(), "%.2f/%.2fMB (%d%%)", + ((double)download.mDownloadedBytes / (double)MEGABYTE), + ((double)download.mSizeBytes / (double)MEGABYTE), + (download.mDownloadedBytes*100)/download.mSizeBytes); + } + + } else { + if (download.mSizeBytes < MEGABYTE) { + return String.format(language.getLocale(), "%.2fKb", ((double)download.mSizeBytes / (double)KILOBYTE)); + + } else { + return String.format(language.getLocale(), "%.2fMB", ((double)download.mSizeBytes / (double)MEGABYTE)); + } + } + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/downloads/DownloadJob.java b/app/src/common/shared/org/mozilla/vrbrowser/downloads/DownloadJob.java new file mode 100644 index 000000000..fb8e107ff --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/downloads/DownloadJob.java @@ -0,0 +1,159 @@ +package org.mozilla.vrbrowser.downloads; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement; +import org.mozilla.geckoview.GeckoSession.WebResponseInfo; + +import java.io.File; +import java.net.URL; + +public class DownloadJob { + + private String mUri; + private String mContentType; + private long mContentLength; + private String mFilename; + private String mTitle; + private String mDescription; + + public static DownloadJob create(@NonNull String uri, @Nullable String contentType, + long contentLength, @Nullable String filename) { + DownloadJob job = new DownloadJob(); + job.mUri = uri; + job.mContentType = contentType; + job.mContentLength = contentLength; + if (filename != null) { + job.mFilename = filename; + } else { + try { + File f = new File(new URL(uri).getPath()); + job.mFilename = f.getName(); + + } catch (Exception e) { + job.mFilename = "Untitled"; + } + } + job.mTitle = filename; + job.mDescription = filename; + return job; + } + + public static DownloadJob from(@NonNull WebResponseInfo response) { + DownloadJob job = new DownloadJob(); + job.mUri = response.uri; + job.mContentType = response.contentType; + job.mContentLength = response.contentLength; + if (response.filename != null && !response.filename.isEmpty()) { + job.mFilename = response.filename; + + } else { + try { + File f = new File(new URL(response.uri).getPath()); + job.mFilename = f.getName(); + + } catch (Exception e) { + job.mFilename = "Unknown"; + } + } + job.mTitle = response.filename; + job.mDescription = response.filename; + return job; + } + + public static DownloadJob fromSrc(@NonNull ContextElement contextElement) { + DownloadJob job = new DownloadJob(); + job.mUri = contextElement.srcUri; + switch (contextElement.type) { + case ContextElement.TYPE_NONE: + job.mContentType = ""; + break; + case ContextElement.TYPE_AUDIO: + job.mContentType = "audio"; + break; + case ContextElement.TYPE_IMAGE: + job.mContentType = "image"; + break; + case ContextElement.TYPE_VIDEO: + job.mContentType = "video"; + break; + } + try { + File f = new File(new URL(contextElement.srcUri).getPath()); + job.mFilename = f.getName(); + + } catch (Exception e) { + job.mFilename = "Unknown"; + } + job.mContentLength = 0; + job.mTitle = job.mFilename; + job.mDescription = job.mFilename; + return job; + } + + public static DownloadJob fromLink(@NonNull ContextElement contextElement) { + DownloadJob job = new DownloadJob(); + job.mUri = contextElement.linkUri; + switch (contextElement.type) { + case ContextElement.TYPE_NONE: + job.mContentType = ""; + break; + case ContextElement.TYPE_AUDIO: + job.mContentType = "audio"; + break; + case ContextElement.TYPE_IMAGE: + job.mContentType = "image"; + break; + case ContextElement.TYPE_VIDEO: + job.mContentType = "video"; + break; + } + try { + File f = new File(new URL(contextElement.linkUri).getPath()); + job.mFilename = f.getName(); + + } catch (Exception e) { + job.mFilename = "Unknown"; + } + job.mContentLength = 0; + job.mTitle = job.mFilename; + job.mDescription = job.mFilename; + return job; + } + + @NonNull + public String getUri() { + return mUri; + } + + @Nullable + public String getContentType() { + return mContentType; + } + + public long getContentLength() { + return mContentLength; + } + + @NonNull + public String getFilename() { + return mFilename; + } + + public String getTitle() { + return mTitle; + } + + public void setTitle(String mTitle) { + this.mTitle = mTitle; + } + + public String getDescription() { + return mDescription; + } + + public void setDescription(String mDescription) { + this.mDescription = mDescription; + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/downloads/DownloadsManager.java b/app/src/common/shared/org/mozilla/vrbrowser/downloads/DownloadsManager.java new file mode 100644 index 000000000..c4278172a --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/downloads/DownloadsManager.java @@ -0,0 +1,229 @@ +package org.mozilla.vrbrowser.downloads; + +import android.app.DownloadManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.Cursor; +import android.net.Uri; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.webkit.URLUtil; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.utils.UrlUtils; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class DownloadsManager { + + private static final String LOGTAG = DownloadsManager.class.getSimpleName(); + + private static final int REFRESH_INTERVAL = 100; + + public interface DownloadsListener { + default void onDownloadsUpdate(@NonNull List downloads) {} + default void onDownloadCompleted(@NonNull Download download) {} + default void onDownloadError(@NonNull String error, @NonNull String file) {} + } + + private Handler mMainHandler; + private Context mContext; + private List mListeners; + private DownloadManager mDownloadManager; + private ScheduledThreadPoolExecutor mExecutor; + + public DownloadsManager(@NonNull Context context) { + mMainHandler = new Handler(Looper.getMainLooper()); + mContext = context; + mListeners = new ArrayList<>(); + mDownloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE); + mExecutor = new ScheduledThreadPoolExecutor(1); + } + + public void init() { + mContext.registerReceiver(mDownloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)); + List downloads = getDownloads(); + downloads.forEach(download -> { + if (!new File(UrlUtils.stripProtocol(download.getOutputFile())).exists()) { + mDownloadManager.remove(download.getId()); + } + }); + } + + public void end() { + mContext.unregisterReceiver(mDownloadReceiver); + } + + public void addListener(@NonNull DownloadsListener listener) { + mListeners.add(listener); + if (mListeners.size() == 1) { + mExecutor.scheduleAtFixedRate(mDownloadUpdateTask, 0, REFRESH_INTERVAL, TimeUnit.MILLISECONDS); + } + } + + public void removeListener(@NonNull DownloadsListener listener) { + mListeners.remove(listener); + if (mListeners.size() == 0) { + mExecutor.remove(mDownloadUpdateTask); + } + } + + public void startDownload(@NonNull DownloadJob job) { + if (!URLUtil.isHttpUrl(job.getUri()) && !URLUtil.isHttpsUrl(job.getUri())) { + notifyDownloadError("Cannot download non http/https files", job.getFilename()); + return; + } + + Uri url = Uri.parse(job.getUri()); + DownloadManager.Request request = new DownloadManager.Request(url); + request.setTitle(job.getTitle()); + request.setDescription(job.getDescription()); + request.setMimeType(job.getContentType()); + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + request.setVisibleInDownloadsUi(false); + if (SettingsStore.getInstance(mContext).getDownloadsStorage() == SettingsStore.EXTERNAL) { + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, job.getFilename()); + + } else { + String outputPath = getOutputPathForJob(job); + if (outputPath == null) { + notifyDownloadError("Cannot create output file", job.getFilename()); + return; + } + request.setDestinationUri(Uri.parse(outputPath)); + } + + mDownloadManager.enqueue(request); + } + + @Nullable + private String getOutputPathForJob(@NonNull DownloadJob job) { + File outputFolder = new File(mContext.getExternalFilesDir(null), Environment.DIRECTORY_DOWNLOADS); + if (outputFolder.exists() || (!outputFolder.exists() && outputFolder.mkdir())) { + File outputFile = new File(outputFolder, job.getFilename()); + return "file://" + outputFile.getAbsolutePath(); + } + + return null; + } + + public void removeDownload(long downloadId) { + mDownloadManager.remove(downloadId); + notifyDownloadsUpdate(); + } + + public void removeAllDownloads() { + if (getDownloads().size() > 0) { + mDownloadManager.remove(getDownloads().stream().mapToLong(Download::getId).toArray()); + notifyDownloadsUpdate(); + } + } + + public void clearDownload(long downloadId) { + Download download = getDownload(downloadId); + if (download != null) { + File file = new File(UrlUtils.stripProtocol(download.getOutputFile())); + if (file.exists()) { + File newFile = new File(UrlUtils.stripProtocol(download.getOutputFile().concat(".bak"))); + file.renameTo(newFile); + mDownloadManager.remove(downloadId); + newFile.renameTo(file); + } + } + notifyDownloadsUpdate(); + } + + public void clearAllDownloads() { + getDownloads().forEach(download -> { + clearDownload(download.getId()); + }); + notifyDownloadsUpdate(); + } + + @Nullable + public Download getDownload(long downloadId) { + Download download = null; + + DownloadManager.Query query = new DownloadManager.Query(); + query.setFilterById(downloadId); + Cursor c = mDownloadManager.query(query); + if (c.moveToFirst()) { + download = Download.from(c); + } + c.close(); + + return download; + } + + public List getDownloads() { + List downloads = new ArrayList<>(); + + DownloadManager.Query query = new DownloadManager.Query(); + Cursor c = mDownloadManager.query(query); + while (c.moveToNext()) { + downloads.add(Download.from(c)); + } + c.close(); + + return downloads; + } + + private BroadcastReceiver mDownloadReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0); + + if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) { + DownloadManager.Query query = new DownloadManager.Query(); + query.setFilterById(downloadId); + Cursor c = mDownloadManager.query(query); + if (c.moveToFirst()) { + notifyDownloadsUpdate(); + notifyDownloadCompleted(Download.from(c)); + } + c.close(); + } + } + }; + + private void notifyDownloadsUpdate() { + List downloads = getDownloads(); + mListeners.forEach(listener -> listener.onDownloadsUpdate(downloads)); + } + + private void notifyDownloadCompleted(@NonNull Download download) { + mListeners.forEach(listener -> listener.onDownloadCompleted(download)); + } + + private void notifyDownloadError(@NonNull String error, @NonNull String file) { + mListeners.forEach(listener -> listener.onDownloadError(error, file)); + } + + private Runnable mDownloadUpdateTask = new Runnable() { + @Override + public void run() { + DownloadManager.Query query = new DownloadManager.Query(); + Cursor c = mDownloadManager.query(query); + + while (c.moveToNext()) { + int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS)); + if (status == DownloadManager.STATUS_RUNNING) { + mMainHandler.post(() -> notifyDownloadsUpdate()); + } + } + c.close(); + } + }; + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java index dbceba2f4..dcbc0c1a0 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/telemetry/GleanMetricsService.java @@ -205,10 +205,11 @@ public enum TabSource { TABS_DIALOG, // Tab opened from the tabs dialog BOOKMARKS, // Tab opened from the bookmarks panel HISTORY, // Tab opened from the history panel + DOWNLOADS, // Tab opened from the downloads panel FXA_LOGIN, // Tab opened by the FxA login flow RECEIVED, // Tab opened by FxA when a tab is received PRE_EXISTING, // Tab opened as a result of restoring the last session - BROWSER // Tab opened by the browser as a result of a new window open + BROWSER, // Tab opened by the browser as a result of a new window open } public static void openedCounter(@NonNull TabSource source) { diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BindingAdapters.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BindingAdapters.java index c836bca2f..327c3152c 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BindingAdapters.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/BindingAdapters.java @@ -14,8 +14,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.databinding.BindingAdapter; +import androidx.databinding.ObservableList; +import androidx.recyclerview.widget.RecyclerView; import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.downloads.Download; import org.mozilla.vrbrowser.ui.views.HoneycombButton; import org.mozilla.vrbrowser.ui.views.UIButton; import org.mozilla.vrbrowser.ui.views.UITextButton; @@ -195,4 +198,12 @@ public static void setTooltipText(@NonNull UIButton button, @Nullable String tex button.setTooltipText(text); } + @BindingAdapter("items") + public static void bindAdapterWithDefaultBinder(@NonNull RecyclerView recyclerView, @Nullable ObservableList items) { + DownloadsAdapter adapter = (DownloadsAdapter)recyclerView.getAdapter(); + if (adapter != null) { + adapter.setDownloadsList(items); + } + } + } \ No newline at end of file diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/DownloadsAdapter.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/DownloadsAdapter.java new file mode 100644 index 000000000..fb5dc3641 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/adapters/DownloadsAdapter.java @@ -0,0 +1,266 @@ +package org.mozilla.vrbrowser.ui.adapters; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.databinding.DataBindingUtil; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.RecyclerView; + +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.databinding.DownloadItemBinding; +import org.mozilla.vrbrowser.downloads.Download; +import org.mozilla.vrbrowser.ui.callbacks.DownloadItemCallback; +import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; +import org.mozilla.vrbrowser.utils.AnimationHelper; +import org.mozilla.vrbrowser.utils.SystemUtils; + +import java.util.List; +import java.util.Objects; + +public class DownloadsAdapter extends RecyclerView.Adapter { + + static final String LOGTAG = SystemUtils.createLogtag(DownloadsAdapter.class); + + private static final int ICON_ANIMATION_DURATION = 200; + + private List mDownloadsList; + + private int mMinPadding; + private int mMaxPadding; + private boolean mIsNarrowLayout; + + @Nullable + private final DownloadItemCallback mDownloadItemCallback; + + public DownloadsAdapter(@Nullable DownloadItemCallback clickCallback, Context aContext) { + mDownloadItemCallback = clickCallback; + + mMinPadding = WidgetPlacement.pixelDimension(aContext, R.dimen.library_icon_padding_min); + mMaxPadding = WidgetPlacement.pixelDimension(aContext, R.dimen.library_icon_padding_max); + + mIsNarrowLayout = false; + + setHasStableIds(true); + } + + public void setNarrow(boolean isNarrow) { + if (mIsNarrowLayout != isNarrow) { + mIsNarrowLayout = isNarrow; + notifyDataSetChanged(); + } + } + + public void setDownloadsList(final List downloadsList) { + if (mDownloadsList == null) { + mDownloadsList = downloadsList; + notifyItemRangeInserted(0, downloadsList.size()); + + } else { + DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() { + @Override + public int getOldListSize() { + return mDownloadsList.size(); + } + + @Override + public int getNewListSize() { + return downloadsList.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + return mDownloadsList.get(oldItemPosition).getUri().equals(downloadsList.get(newItemPosition).getUri()); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + Download newDownloadItem = downloadsList.get(newItemPosition); + Download oldDownloadItem = mDownloadsList.get(oldItemPosition); + return newDownloadItem.getLastModified() == oldDownloadItem.getLastModified() + && Objects.equals(newDownloadItem.getTitle(), oldDownloadItem.getTitle()) + && Objects.equals(newDownloadItem.getDescription(), oldDownloadItem.getDescription()) + && Objects.equals(newDownloadItem.getOutputFile(), oldDownloadItem.getOutputFile()) + && Objects.equals(newDownloadItem.getUri(), oldDownloadItem.getUri()); + } + }); + + mDownloadsList = downloadsList; + result.dispatchUpdatesTo(this); + } + } + + public void removeItem(Download downloadItem) { + int position = mDownloadsList.indexOf(downloadItem); + if (position >= 0) { + mDownloadsList.remove(position); + notifyItemRemoved(position); + } + } + + public int itemCount() { + if (mDownloadsList != null) { + return mDownloadsList.size(); + } + + return 0; + } + + public int getItemPosition(long id) { + for (int position=0; position { + int ev = motionEvent.getActionMasked(); + switch (ev) { + case MotionEvent.ACTION_HOVER_ENTER: + binding.setIsHovered(true); + return false; + + case MotionEvent.ACTION_HOVER_EXIT: + binding.setIsHovered(false); + return false; + } + + return false; + }); + binding.layout.setOnTouchListener((view, motionEvent) -> { + int ev = motionEvent.getActionMasked(); + switch (ev) { + case MotionEvent.ACTION_UP: + return false; + + case MotionEvent.ACTION_DOWN: + binding.more.setImageState(new int[]{android.R.attr.state_active},false); + binding.trash.setImageState(new int[]{android.R.attr.state_active},false); + binding.setIsHovered(true); + return false; + + case MotionEvent.ACTION_CANCEL: + binding.setIsHovered(false); + return false; + } + return false; + }); + binding.more.setOnHoverListener(mIconHoverListener); + binding.more.setOnTouchListener((view, motionEvent) -> { + binding.setIsHovered(true); + int ev = motionEvent.getActionMasked(); + switch (ev) { + case MotionEvent.ACTION_UP: + if (mDownloadItemCallback != null) { + mDownloadItemCallback.onMore(view, binding.getItem()); + } + binding.more.setImageState(new int[]{android.R.attr.state_active},true); + return true; + + case MotionEvent.ACTION_DOWN: + binding.more.setImageState(new int[]{android.R.attr.state_pressed},true); + return true; + + case MotionEvent.ACTION_CANCEL: + binding.setIsHovered(false); + binding.more.setImageState(new int[]{android.R.attr.state_active},true); + return false; + } + return false; + }); + binding.trash.setOnHoverListener(mIconHoverListener); + binding.trash.setOnTouchListener((view, motionEvent) -> { + binding.setIsHovered(true); + int ev = motionEvent.getActionMasked(); + switch (ev) { + case MotionEvent.ACTION_UP: + if (mDownloadItemCallback != null) { + mDownloadItemCallback.onDelete(view, binding.getItem()); + } + binding.trash.setImageState(new int[]{android.R.attr.state_active},true); + return true; + + case MotionEvent.ACTION_DOWN: + binding.trash.setImageState(new int[]{android.R.attr.state_pressed},true); + return true; + + case MotionEvent.ACTION_CANCEL: + binding.setIsHovered(false); + binding.trash.setImageState(new int[]{android.R.attr.state_active},true); + return false; + } + return false; + }); + + return new DownloadItemViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + DownloadItemViewHolder item = (DownloadItemViewHolder) holder; + item.binding.setItem(mDownloadsList.get(position)); + item.binding.setIsNarrow(mIsNarrowLayout); + } + + @Override + public int getItemCount() { + return mDownloadsList == null ? 0 : mDownloadsList.size(); + } + + @Override + public long getItemId(int position) { + Download download = mDownloadsList.get(position); + return download.getId(); + } + + private View.OnHoverListener mIconHoverListener = (view, motionEvent) -> { + ImageView icon = (ImageView)view; + int ev = motionEvent.getActionMasked(); + switch (ev) { + case MotionEvent.ACTION_HOVER_ENTER: + icon.setImageState(new int[]{android.R.attr.state_hovered},true); + AnimationHelper.animateViewPadding(view, + mMaxPadding, + mMinPadding, + ICON_ANIMATION_DURATION); + return false; + + case MotionEvent.ACTION_HOVER_EXIT: + icon.setImageState(new int[]{android.R.attr.state_active},true); + AnimationHelper.animateViewPadding(view, + mMinPadding, + mMaxPadding, + ICON_ANIMATION_DURATION); + return false; + } + + return false; + }; + + static class DownloadItemViewHolder extends RecyclerView.ViewHolder { + + final DownloadItemBinding binding; + + DownloadItemViewHolder(@NonNull DownloadItemBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarksCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarksCallback.java index 26f0002ae..3de3456bf 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarksCallback.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/BookmarksCallback.java @@ -7,11 +7,9 @@ import org.mozilla.vrbrowser.ui.adapters.Bookmark; public interface BookmarksCallback { - default void onClearBookmarks(@NonNull View view) {} default void onSyncBookmarks(@NonNull View view) {} default void onFxALogin(@NonNull View view) {} default void onFxASynSettings(@NonNull View view) {} default void onShowContextMenu(@NonNull View view, Bookmark item, boolean isLastVisibleItem) {} default void onHideContextMenu(@NonNull View view) {} - default void onClickItem(@NonNull View view, Bookmark item) {} } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/DownloadItemCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/DownloadItemCallback.java new file mode 100644 index 000000000..b11f0b1d0 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/DownloadItemCallback.java @@ -0,0 +1,14 @@ +package org.mozilla.vrbrowser.ui.callbacks; + +import android.view.View; + +import androidx.annotation.NonNull; + +import org.mozilla.vrbrowser.downloads.Download; +import org.mozilla.vrbrowser.ui.adapters.Bookmark; + +public interface DownloadItemCallback { + void onClick(@NonNull View view, @NonNull Download item); + void onDelete(@NonNull View view, @NonNull Download item); + void onMore(@NonNull View view, @NonNull Download item); +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/DownloadsCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/DownloadsCallback.java new file mode 100644 index 000000000..fcfe7e33d --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/DownloadsCallback.java @@ -0,0 +1,15 @@ +package org.mozilla.vrbrowser.ui.callbacks; + +import android.view.View; + +import androidx.annotation.NonNull; + +import org.mozilla.vrbrowser.downloads.Download; +import org.mozilla.vrbrowser.ui.adapters.Bookmark; + +public interface DownloadsCallback { + default void onDeleteDownloads(@NonNull View view) {} + default void onShowContextMenu(@NonNull View view, Download item, boolean isLastVisibleItem) {} + default void onHideContextMenu(@NonNull View view) {} + default void onShowSortingContextMenu(@NonNull View view) {} +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/DownloadsContextMenuCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/DownloadsContextMenuCallback.java new file mode 100644 index 000000000..bf246c16a --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/DownloadsContextMenuCallback.java @@ -0,0 +1,7 @@ +package org.mozilla.vrbrowser.ui.callbacks; + +import org.mozilla.vrbrowser.ui.widgets.menus.library.DownloadsContextMenuWidget; + +public interface DownloadsContextMenuCallback extends LibraryContextMenuCallback { + void onDelete(DownloadsContextMenuWidget.DownloadsContextMenuItem item); +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryCallback.java index 6125ba8b1..5070d582a 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryCallback.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryCallback.java @@ -13,5 +13,4 @@ default void onFxALogin(@NonNull View view) {} default void onFxASynSettings(@NonNull View view) {} default void onShowContextMenu(@NonNull View view, @NonNull VisitInfo item, boolean isLastVisibleItem) {} default void onHideContextMenu(@NonNull View view) {} - default void onClickItem(@NonNull View view, @NonNull VisitInfo item) {} } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryContextMenuCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryContextMenuCallback.java new file mode 100644 index 000000000..7ec900aa6 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/HistoryContextMenuCallback.java @@ -0,0 +1,8 @@ +package org.mozilla.vrbrowser.ui.callbacks; + +import org.mozilla.vrbrowser.ui.widgets.menus.library.HistoryContextMenuWidget; + +public interface HistoryContextMenuCallback extends LibraryContextMenuCallback { + void onAddToBookmarks(HistoryContextMenuWidget.LibraryContextMenuItem item); + void onRemoveFromBookmarks(HistoryContextMenuWidget.LibraryContextMenuItem item); +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/LibraryContextMenuCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/LibraryContextMenuCallback.java new file mode 100644 index 000000000..bfef4dadf --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/LibraryContextMenuCallback.java @@ -0,0 +1,8 @@ +package org.mozilla.vrbrowser.ui.callbacks; + +import org.mozilla.vrbrowser.ui.widgets.menus.library.LibraryContextMenuWidget; + +public interface LibraryContextMenuCallback { + void onOpenInNewWindowClick(LibraryContextMenuWidget.LibraryContextMenuItem item); + void onOpenInNewTabClick(LibraryContextMenuWidget.LibraryContextMenuItem item); +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/LibraryItemContextMenuClickCallback.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/LibraryItemContextMenuClickCallback.java deleted file mode 100644 index f0cf2e9c5..000000000 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/callbacks/LibraryItemContextMenuClickCallback.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.mozilla.vrbrowser.ui.callbacks; - -import org.mozilla.vrbrowser.ui.widgets.menus.LibraryMenuWidget; - -public interface LibraryItemContextMenuClickCallback { - void onOpenInNewWindowClick(LibraryMenuWidget.LibraryContextMenuItem item); - void onOpenInNewTabClick(LibraryMenuWidget.LibraryContextMenuItem item); - void onAddToBookmarks(LibraryMenuWidget.LibraryContextMenuItem item); - void onRemoveFromBookmarks(LibraryMenuWidget.LibraryContextMenuItem item); -} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/BookmarksViewModel.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/BookmarksViewModel.java index 6fe7b5b32..3dd206e03 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/BookmarksViewModel.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/BookmarksViewModel.java @@ -3,45 +3,10 @@ import android.app.Application; import androidx.annotation.NonNull; -import androidx.databinding.ObservableBoolean; -import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.MutableLiveData; -public class BookmarksViewModel extends AndroidViewModel { - - private MutableLiveData isLoading; - private MutableLiveData isEmpty; - private MutableLiveData isNarrow; +public class BookmarksViewModel extends LibraryViewModel { public BookmarksViewModel(@NonNull Application application) { super(application); - - isLoading = new MutableLiveData<>(new ObservableBoolean(false)); - isEmpty = new MutableLiveData<>(new ObservableBoolean(false)); - isNarrow = new MutableLiveData<>(new ObservableBoolean(false)); - } - - public MutableLiveData getIsLoading() { - return isLoading; - } - - public void setIsLoading(boolean isLoading) { - this.isLoading.setValue(new ObservableBoolean(isLoading)); - } - - public MutableLiveData getIsEmpty() { - return isEmpty; - } - - public void setIsEmpty(boolean isEmpty) { - this.isEmpty.setValue(new ObservableBoolean(isEmpty)); - } - - public MutableLiveData getIsNarrow() { - return isNarrow; - } - - public void setIsNarrow(boolean isNarrow) { - this.isNarrow.setValue(new ObservableBoolean(isNarrow)); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/DownloadsViewModel.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/DownloadsViewModel.java new file mode 100644 index 000000000..d5926233a --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/DownloadsViewModel.java @@ -0,0 +1,12 @@ +package org.mozilla.vrbrowser.ui.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; + +public class DownloadsViewModel extends LibraryViewModel { + + public DownloadsViewModel(@NonNull Application application) { + super(application); + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/HistoryViewModel.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/HistoryViewModel.java index d7a9ecdf5..ae03947fe 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/HistoryViewModel.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/HistoryViewModel.java @@ -3,45 +3,10 @@ import android.app.Application; import androidx.annotation.NonNull; -import androidx.databinding.ObservableBoolean; -import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.MutableLiveData; -public class HistoryViewModel extends AndroidViewModel { - - private MutableLiveData isLoading; - private MutableLiveData isEmpty; - private MutableLiveData isNarrow; +public class HistoryViewModel extends LibraryViewModel { public HistoryViewModel(@NonNull Application application) { super(application); - - isLoading = new MutableLiveData<>(new ObservableBoolean(false)); - isEmpty = new MutableLiveData<>(new ObservableBoolean(false)); - isNarrow = new MutableLiveData<>(new ObservableBoolean(false)); - } - - public MutableLiveData getIsLoading() { - return isLoading; - } - - public void setIsLoading(boolean isLoading) { - this.isLoading.setValue(new ObservableBoolean(isLoading)); - } - - public MutableLiveData getIsEmpty() { - return isEmpty; - } - - public void setIsEmpty(boolean isEmpty) { - this.isEmpty.setValue(new ObservableBoolean(isEmpty)); - } - - public MutableLiveData getIsNarrow() { - return isNarrow; - } - - public void setIsNarrow(boolean isNarrow) { - this.isNarrow.setValue(new ObservableBoolean(isNarrow)); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/LibraryViewModel.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/LibraryViewModel.java new file mode 100644 index 000000000..3821841ac --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/LibraryViewModel.java @@ -0,0 +1,47 @@ +package org.mozilla.vrbrowser.ui.viewmodel; + +import android.app.Application; + +import androidx.annotation.NonNull; +import androidx.databinding.ObservableBoolean; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.MutableLiveData; + +public class LibraryViewModel extends AndroidViewModel { + + private MutableLiveData isLoading; + private MutableLiveData isEmpty; + private MutableLiveData isNarrow; + + public LibraryViewModel(@NonNull Application application) { + super(application); + + isLoading = new MutableLiveData<>(new ObservableBoolean(false)); + isEmpty = new MutableLiveData<>(new ObservableBoolean(false)); + isNarrow = new MutableLiveData<>(new ObservableBoolean(false)); + } + + public MutableLiveData getIsLoading() { + return isLoading; + } + + public void setIsLoading(boolean isLoading) { + this.isLoading.setValue(new ObservableBoolean(isLoading)); + } + + public MutableLiveData getIsEmpty() { + return isEmpty; + } + + public void setIsEmpty(boolean isEmpty) { + this.isEmpty.setValue(new ObservableBoolean(isEmpty)); + } + + public MutableLiveData getIsNarrow() { + return isNarrow; + } + + public void setIsNarrow(boolean isNarrow) { + this.isNarrow.setValue(new ObservableBoolean(isNarrow)); + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/TrayViewModel.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/TrayViewModel.java index 9ee832162..c9bf888f0 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/TrayViewModel.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/TrayViewModel.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import androidx.databinding.ObservableBoolean; +import androidx.databinding.ObservableInt; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MutableLiveData; @@ -14,6 +15,7 @@ public class TrayViewModel extends AndroidViewModel { private MutableLiveData isMaxWindows; private MutableLiveData shouldBeVisible; private MutableLiveData isKeyboardVisible; + private MutableLiveData downloadsNumber; private MediatorLiveData isVisible; public TrayViewModel(@NonNull Application application) { @@ -22,6 +24,7 @@ public TrayViewModel(@NonNull Application application) { isMaxWindows = new MutableLiveData<>(new ObservableBoolean(true)); shouldBeVisible = new MutableLiveData<>(new ObservableBoolean(true)); isKeyboardVisible = new MutableLiveData<>(new ObservableBoolean(false)); + downloadsNumber = new MutableLiveData<>(new ObservableInt(0)); isVisible = new MediatorLiveData<>(); isVisible.addSource(shouldBeVisible, mIsVisibleObserver); isVisible.addSource(isKeyboardVisible, mIsVisibleObserver); @@ -69,4 +72,12 @@ public MutableLiveData getIsVisible() { return isVisible; } + public void setDownloadsNumber(int number) { + this.downloadsNumber.setValue(new ObservableInt(number)); + } + + public MutableLiveData getDownloadsNumber() { + return downloadsNumber; + } + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/WindowViewModel.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/WindowViewModel.java index cdd7d2fa7..42bb23fa0 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/WindowViewModel.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/viewmodel/WindowViewModel.java @@ -16,7 +16,6 @@ import androidx.lifecycle.Observer; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.VRBrowserApplication; import org.mozilla.vrbrowser.browser.SettingsStore; import org.mozilla.vrbrowser.ui.widgets.Windows; import org.mozilla.vrbrowser.utils.ServoUtils; @@ -24,12 +23,9 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; -import java.util.concurrent.Executor; public class WindowViewModel extends AndroidViewModel { - private Executor mUIThreadExecutor; - private int mURLProtocolColor; private int mURLWebsiteColor; @@ -48,6 +44,7 @@ public class WindowViewModel extends AndroidViewModel { private MediatorLiveData isTitleBarVisible; private MutableLiveData isBookmarksVisible; private MutableLiveData isHistoryVisible; + private MutableLiveData isDownloadsVisible; private MediatorLiveData isLibraryVisible; private MutableLiveData isLoading; private MutableLiveData isMicrophoneEnabled; @@ -74,8 +71,6 @@ public class WindowViewModel extends AndroidViewModel { public WindowViewModel(Application application) { super(application); - mUIThreadExecutor = ((VRBrowserApplication)application).getExecutors().mainThread(); - TypedValue typedValue = new TypedValue(); Resources.Theme theme = application.getTheme(); theme.resolveAttribute(R.attr.urlProtocolColor, typedValue, true); @@ -121,10 +116,12 @@ public WindowViewModel(Application application) { isBookmarksVisible = new MutableLiveData<>(new ObservableBoolean(false)); isHistoryVisible = new MutableLiveData<>(new ObservableBoolean(false)); + isDownloadsVisible = new MutableLiveData<>(new ObservableBoolean(false)); isLibraryVisible = new MediatorLiveData<>(); isLibraryVisible.addSource(isBookmarksVisible, mIsLibraryVisibleObserver); isLibraryVisible.addSource(isHistoryVisible, mIsLibraryVisibleObserver); + isLibraryVisible.addSource(isDownloadsVisible, mIsLibraryVisibleObserver); isLibraryVisible.setValue(new ObservableBoolean(false)); isLoading = new MutableLiveData<>(new ObservableBoolean(false)); @@ -208,7 +205,11 @@ public void onChanged(ObservableBoolean o) { private Observer mIsLibraryVisibleObserver = new Observer() { @Override public void onChanged(ObservableBoolean o) { - isLibraryVisible.postValue(new ObservableBoolean(isBookmarksVisible.getValue().get() || isHistoryVisible.getValue().get())); + isLibraryVisible.postValue(new ObservableBoolean( + isBookmarksVisible.getValue().get() || + isHistoryVisible.getValue().get() || + isDownloadsVisible.getValue().get() + )); // We use this to force dispatch a title bar and navigation bar URL refresh when library is opened url.postValue(url.getValue()); @@ -234,6 +235,9 @@ public void onChanged(Spannable aUrl) { } else if (isHistoryVisible.getValue().get()) { url = getApplication().getString(R.string.url_history_title); + } else if (isDownloadsVisible.getValue().get()) { + url = getApplication().getString(R.string.url_downloads_title); + } else { if (UrlUtils.isPrivateAboutPage(getApplication(), url) || (UrlUtils.isDataUri(url) && isPrivateSession.getValue().get())) { @@ -258,6 +262,7 @@ public void onChanged(ObservableBoolean o) { if (isInsecure.getValue().get()) { if (UrlUtils.isPrivateAboutPage(getApplication(), aUrl) || (UrlUtils.isDataUri(aUrl) && isPrivateSession.getValue().get()) || + UrlUtils.isFileUri(aUrl) || UrlUtils.isHomeUri(getApplication(), aUrl) || isLibraryVisible.getValue().get() || UrlUtils.isBlankUri(getApplication(), aUrl)) { @@ -403,6 +408,9 @@ private String getHintValue() { } else if (isHistoryVisible.getValue().get()) { return getApplication().getString(R.string.url_history_title); + } else if (isDownloadsVisible.getValue().get()) { + return getApplication().getString(R.string.url_downloads_title); + } else { return getApplication().getString(R.string.search_placeholder); } @@ -521,6 +529,29 @@ public void setIsHistoryVisible(boolean isHistoryVisible) { this.isHistoryVisible.postValue(new ObservableBoolean(isHistoryVisible)); } + @NonNull + public MutableLiveData getIsDownloadsVisible() { + return isDownloadsVisible; + } + + public void setIsDownloadsVisible(boolean isDownloadsVisible) { + this.isDownloadsVisible.postValue(new ObservableBoolean(isDownloadsVisible)); + } + + public void setIsPanelVisible(@NonNull Windows.PanelType panelType, boolean isVisible) { + switch (panelType) { + case BOOKMARKS: + setIsBookmarksVisible(isVisible); + break; + case HISTORY: + setIsHistoryVisible(isVisible); + break; + case DOWNLOADS: + setIsDownloadsVisible(isVisible); + break; + } + } + @NonNull public MutableLiveData getIsLibraryVisible() { return isLibraryVisible; diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/UIButton.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/UIButton.java index 26cfe3709..d47722130 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/UIButton.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/UIButton.java @@ -5,12 +5,17 @@ package org.mozilla.vrbrowser.ui.views; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; +import android.graphics.drawable.ClipDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; import android.os.Build; import android.util.AttributeSet; import android.util.TypedValue; @@ -51,11 +56,15 @@ public class UIButton extends AppCompatImageButton implements CustomUIButton { private boolean mIsPrivate; private boolean mIsActive; private boolean mIsNotification; + private ClipDrawable mClipDrawable; + private Drawable mDrawable; + private int mClipColor; public UIButton(Context context, AttributeSet attrs) { this(context, attrs, R.attr.imageButtonStyle); } + @SuppressLint("ResourceType") public UIButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); @@ -80,12 +89,21 @@ public UIButton(Context context, AttributeSet attrs, int defStyleAttr) { mTooltipText = arr.getString(0); } mTooltipLayout = attributes.getResourceId(R.styleable.UIButton_tooltipLayout, R.layout.tooltip); + mClipDrawable = (ClipDrawable)attributes.getDrawable(R.styleable.UIButton_clipDrawable); + mClipColor = attributes.getColor(R.styleable.UIButton_clipColor, 0); attributes.recycle(); if (mBackground == null) { mBackground = getBackground(); } + if (mClipDrawable != null) { + Drawable[] layers = new Drawable[] { mDrawable, mClipDrawable }; + setImageDrawable(new LayerDrawable(layers)); + mClipDrawable.setLevel(0); + mClipDrawable.setTint(R.color.azure); + } + // Android >8 doesn't perform a click when long clicking in ImageViews even if long click is disabled if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { setLongClickable(false); @@ -161,21 +179,31 @@ public boolean onHoverEvent(MotionEvent event) { } public void setTintColorList(int aColorListId) { + if (mDrawable == null) { + return; + } mTintColorList = getContext().getResources().getColorStateList( aColorListId, getContext().getTheme()); - if (mTintColorList != null) { - int color = mTintColorList.getColorForState(getDrawableState(), 0); - setColorFilter(color); + int color = mTintColorList.getColorForState(getDrawableState(), 0); + mDrawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + if (mClipDrawable != null) { + mClipDrawable.setColorFilter(new PorterDuffColorFilter(mClipColor, PorterDuff.Mode.MULTIPLY)); } } @Override protected void drawableStateChanged() { super.drawableStateChanged(); + if (mDrawable == null) { + return; + } if (mTintColorList != null && mTintColorList.isStateful()) { int color = mTintColorList.getColorForState(getDrawableState(), 0); - setColorFilter(color); + mDrawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.MULTIPLY)); + if (mClipDrawable != null) { + mClipDrawable.setColorFilter(new PorterDuffColorFilter(mClipColor, PorterDuff.Mode.MULTIPLY)); + } } } @@ -337,4 +365,14 @@ public void setEnabled(boolean enabled) { setHovered(false); } + + @Override + public void setImageDrawable(@Nullable Drawable drawable) { + super.setImageDrawable(drawable); + mDrawable = drawable; + } + + public boolean setLevel(int level) { + return mClipDrawable.setLevel(level); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/BookmarksView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/library/BookmarksView.java similarity index 84% rename from app/src/common/shared/org/mozilla/vrbrowser/ui/views/BookmarksView.java rename to app/src/common/shared/org/mozilla/vrbrowser/ui/views/library/BookmarksView.java index f11d4a47a..215d57265 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/BookmarksView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/library/BookmarksView.java @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.vrbrowser.ui.views; +package org.mozilla.vrbrowser.ui.views.library; import android.annotation.SuppressLint; import android.content.Context; @@ -11,7 +11,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; -import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -36,14 +35,17 @@ import org.mozilla.vrbrowser.ui.adapters.CustomLinearLayoutManager; import org.mozilla.vrbrowser.ui.callbacks.BookmarkItemCallback; import org.mozilla.vrbrowser.ui.callbacks.BookmarksCallback; +import org.mozilla.vrbrowser.ui.callbacks.LibraryContextMenuCallback; import org.mozilla.vrbrowser.ui.viewmodel.BookmarksViewModel; import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; +import org.mozilla.vrbrowser.ui.widgets.WindowWidget; +import org.mozilla.vrbrowser.ui.widgets.Windows; +import org.mozilla.vrbrowser.ui.widgets.menus.library.BookmarksContextMenuWidget; +import org.mozilla.vrbrowser.ui.widgets.menus.library.LibraryContextMenuWidget; import org.mozilla.vrbrowser.utils.SystemUtils; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; import mozilla.appservices.places.BookmarkRoot; import mozilla.components.concept.storage.BookmarkNode; @@ -55,44 +57,38 @@ import mozilla.components.service.fxa.sync.SyncReason; import mozilla.components.service.fxa.sync.SyncStatusObserver; -public class BookmarksView extends FrameLayout implements BookmarksStore.BookmarkListener { +import static org.mozilla.vrbrowser.ui.widgets.settings.SettingsView.SettingViewType.FXA; + +public class BookmarksView extends LibraryView implements BookmarksStore.BookmarkListener { private static final String LOGTAG = SystemUtils.createLogtag(BookmarksView.class); private static final boolean ACCOUNTS_UI_ENABLED = false; - private BookmarksViewModel mViewModel; private BookmarksBinding mBinding; private Accounts mAccounts; private BookmarkAdapter mBookmarkAdapter; - private ArrayList mBookmarksViewListeners; private CustomLinearLayoutManager mLayoutManager; - private Executor mUIThreadExecutor; + private BookmarksViewModel mViewModel; public BookmarksView(Context aContext) { super(aContext); - initialize(aContext); + initialize(); } public BookmarksView(Context aContext, AttributeSet aAttrs) { super(aContext, aAttrs); - initialize(aContext); + initialize(); } public BookmarksView(Context aContext, AttributeSet aAttrs, int aDefStyle) { super(aContext, aAttrs, aDefStyle); - initialize(aContext); + initialize(); } - private void initialize(Context aContext) { - mViewModel = new ViewModelProvider( - (VRBrowserActivity)getContext(), - ViewModelProvider.AndroidViewModelFactory.getInstance(((VRBrowserActivity) getContext()).getApplication())) - .get(BookmarksViewModel.class); - - mUIThreadExecutor = ((VRBrowserApplication)getContext().getApplicationContext()).getExecutors().mainThread(); - - mBookmarksViewListeners = new ArrayList<>(); + @Override + protected void initialize() { + super.initialize(); mAccounts = ((VRBrowserApplication)getContext().getApplicationContext()).getAccounts(); if (ACCOUNTS_UI_ENABLED) { @@ -100,6 +96,11 @@ private void initialize(Context aContext) { mAccounts.addSyncListener(mSyncListener); } + mViewModel = new ViewModelProvider( + (VRBrowserActivity)getContext(), + ViewModelProvider.AndroidViewModelFactory.getInstance(((VRBrowserActivity) getContext()).getApplication())) + .get(BookmarksViewModel.class); + SessionStore.get().getBookmarkStore().addListener(this); updateUI(); @@ -113,7 +114,8 @@ public void updateUI() { // Inflate this data binding layout mBinding = DataBindingUtil.inflate(inflater, R.layout.bookmarks, this, true); - + mBinding.setLifecycleOwner((VRBrowserActivity)getContext()); + mBinding.setBookmarksViewModel(mViewModel); mBinding.setCallback(mBookmarksCallback); mBookmarkAdapter = new BookmarkAdapter(mBookmarkItemCallback, getContext()); mBinding.bookmarksList.setAdapter(mBookmarkAdapter); @@ -150,10 +152,12 @@ public void updateUI() { }); } + @Override public void onShow() { updateLayout(); } + @Override public void onDestroy() { SessionStore.get().getBookmarkStore().removeListener(this); @@ -165,17 +169,6 @@ public void onDestroy() { } } - private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - super.onScrolled(recyclerView, dx, dy); - - if (recyclerView.getScrollState() != RecyclerView.SCROLL_STATE_SETTLING) { - recyclerView.requestFocus(); - } - } - }; - private final BookmarkItemCallback mBookmarkItemCallback = new BookmarkItemCallback() { @Override public void onClick(@NonNull View view, @NonNull Bookmark item) { @@ -184,7 +177,8 @@ public void onClick(@NonNull View view, @NonNull Bookmark item) { Session session = SessionStore.get().getActiveSession(); session.loadUri(item.getUrl()); - mBookmarksViewListeners.forEach((listener) -> listener.onClickItem(view, item)); + WindowWidget window = mWidgetManager.getFocusedWindow(); + window.hidePanel(Windows.PanelType.BOOKMARKS); } @Override @@ -225,18 +219,16 @@ public void onFolderOpened(@NonNull Bookmark item) { }; private BookmarksCallback mBookmarksCallback = new BookmarksCallback() { - @Override - public void onClearBookmarks(@NonNull View view) { - mBookmarksViewListeners.forEach((listener) -> listener.onClearBookmarks(view)); - } @Override public void onSyncBookmarks(@NonNull View view) { + view.requestFocusFromTouch(); mAccounts.syncNowAsync(SyncReason.User.INSTANCE, false); } @Override public void onFxALogin(@NonNull View view) { + view.requestFocusFromTouch(); if (mAccounts.getAccountStatus() == Accounts.AccountStatus.SIGNED_IN) { mAccounts.logoutAsync(); @@ -254,7 +246,8 @@ public void onFxALogin(@NonNull View view) { widgetManager.getFocusedWindow().getSession().setUaMode(GeckoSessionSettings.USER_AGENT_MODE_MOBILE); GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.FXA_LOGIN); - mBookmarksViewListeners.forEach((listener) -> listener.onFxALogin(view)); + WindowWidget window = mWidgetManager.getFocusedWindow(); + window.hidePanel(Windows.PanelType.BOOKMARKS); } }, mUIThreadExecutor).exceptionally(throwable -> { @@ -268,24 +261,28 @@ public void onFxALogin(@NonNull View view) { @Override public void onFxASynSettings(@NonNull View view) { - mBookmarksViewListeners.forEach((listener) -> listener.onFxASynSettings(view)); + view.requestFocusFromTouch(); + mWidgetManager.getTray().showSettingsDialog(FXA); } @Override public void onShowContextMenu(@NonNull View view, Bookmark item, boolean isLastVisibleItem) { - mBookmarksViewListeners.forEach((listener) -> listener.onShowContextMenu(view, item, isLastVisibleItem)); + showContextMenu( + view, + new BookmarksContextMenuWidget(getContext(), + new BookmarksContextMenuWidget.LibraryContextMenuItem( + item.getUrl(), + item.getTitle()), + mWidgetManager.canOpenNewWindow()), + mCallback, + isLastVisibleItem); } - }; - public void addBookmarksListener(@NonNull BookmarksCallback listener) { - if (!mBookmarksViewListeners.contains(listener)) { - mBookmarksViewListeners.add(listener); + @Override + public void onHideContextMenu(@NonNull View view) { + hideContextMenu(); } - } - - public void removeBookmarksListener(@NonNull BookmarksCallback listener) { - mBookmarksViewListeners.remove(listener); - } + }; private SyncStatusObserver mSyncListener = new SyncStatusObserver() { @Override @@ -362,6 +359,8 @@ private void showBookmarks(List aBookmarks) { mViewModel.setIsLoading(false); mBookmarkAdapter.setBookmarkList(aBookmarks); } + + mBinding.executePendingBindings(); } @Override @@ -371,7 +370,8 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto updateLayout(); } - private void updateLayout() { + @Override + protected void updateLayout() { post(() -> { double width = Math.ceil(getWidth()/getContext().getResources().getDisplayMetrics().density); boolean isNarrow = width < SettingsStore.WINDOW_WIDTH_DEFAULT; @@ -390,6 +390,21 @@ private void updateLayout() { }); } + private LibraryContextMenuCallback mCallback = new LibraryContextMenuCallback() { + @Override + public void onOpenInNewWindowClick(LibraryContextMenuWidget.LibraryContextMenuItem item) { + mWidgetManager.openNewWindow(item.getUrl()); + hideContextMenu(); + } + + @Override + public void onOpenInNewTabClick(LibraryContextMenuWidget.LibraryContextMenuItem item) { + mWidgetManager.openNewTabForeground(item.getUrl()); + GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.BOOKMARKS); + hideContextMenu(); + } + }; + // BookmarksStore.BookmarksViewListener @Override diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/library/DownloadsView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/library/DownloadsView.java new file mode 100644 index 000000000..d0a3076c1 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/library/DownloadsView.java @@ -0,0 +1,388 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.vrbrowser.ui.views.library; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.PointF; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.databinding.DataBindingUtil; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.VRBrowserActivity; +import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.browser.engine.SessionStore; +import org.mozilla.vrbrowser.databinding.DownloadsBinding; +import org.mozilla.vrbrowser.downloads.Download; +import org.mozilla.vrbrowser.downloads.DownloadsManager; +import org.mozilla.vrbrowser.telemetry.GleanMetricsService; +import org.mozilla.vrbrowser.ui.adapters.DownloadsAdapter; +import org.mozilla.vrbrowser.ui.callbacks.DownloadItemCallback; +import org.mozilla.vrbrowser.ui.callbacks.DownloadsCallback; +import org.mozilla.vrbrowser.ui.callbacks.DownloadsContextMenuCallback; +import org.mozilla.vrbrowser.ui.viewmodel.DownloadsViewModel; +import org.mozilla.vrbrowser.ui.widgets.UIWidget; +import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; +import org.mozilla.vrbrowser.ui.widgets.WindowWidget; +import org.mozilla.vrbrowser.ui.widgets.Windows; +import org.mozilla.vrbrowser.ui.widgets.dialogs.PromptDialogWidget; +import org.mozilla.vrbrowser.ui.widgets.menus.library.DownloadsContextMenuWidget; +import org.mozilla.vrbrowser.ui.widgets.menus.library.LibraryContextMenuWidget; +import org.mozilla.vrbrowser.ui.widgets.menus.library.SortingContextMenuWidget; +import org.mozilla.vrbrowser.utils.SystemUtils; + +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class DownloadsView extends LibraryView implements DownloadsManager.DownloadsListener { + + private static final String LOGTAG = SystemUtils.createLogtag(DownloadsView.class); + + private DownloadsBinding mBinding; + private DownloadsAdapter mDownloadsAdapter; + private DownloadsManager mDownloadsManager; + private Comparator mSortingComparator; + private DownloadsViewModel mViewModel; + + public DownloadsView(Context aContext) { + super(aContext); + initialize(); + } + + public DownloadsView(Context aContext, AttributeSet aAttrs) { + super(aContext, aAttrs); + initialize(); + } + + public DownloadsView(Context aContext, AttributeSet aAttrs, int aDefStyle) { + super(aContext, aAttrs, aDefStyle); + initialize(); + } + + @Override + protected void initialize() { + super.initialize(); + + mDownloadsManager = ((VRBrowserActivity) getContext()).getServicesProvider().getDownloadsManager(); + mViewModel = new ViewModelProvider( + (VRBrowserActivity)getContext(), + ViewModelProvider.AndroidViewModelFactory.getInstance(((VRBrowserActivity) getContext()).getApplication())) + .get(DownloadsViewModel.class); + + mSortingComparator = mAZFileNameComparator; + + updateUI(); + } + + @SuppressLint("ClickableViewAccessibility") + public void updateUI() { + removeAllViews(); + + LayoutInflater inflater = LayoutInflater.from(getContext()); + + // Inflate this data binding layout + mBinding = DataBindingUtil.inflate(inflater, R.layout.downloads, this, true); + mBinding.setLifecycleOwner((VRBrowserActivity)getContext()); + mBinding.setCallback(mDownloadsCallback); + mBinding.setDownloadsViewModel(mViewModel); + mDownloadsAdapter = new DownloadsAdapter(mDownloadItemCallback, getContext()); + mBinding.downloadsList.setAdapter(mDownloadsAdapter); + mBinding.downloadsList.setOnTouchListener((v, event) -> { + v.requestFocusFromTouch(); + return false; + }); + mBinding.downloadsList.addOnScrollListener(mScrollListener); + mBinding.downloadsList.setHasFixedSize(true); + mBinding.downloadsList.setItemViewCacheSize(20); + mBinding.downloadsList.setDrawingCacheEnabled(true); + mBinding.downloadsList.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH); + + mViewModel.setIsEmpty(true); + mViewModel.setIsLoading(true); + mViewModel.setIsNarrow(false); + + onDownloadsUpdate(mDownloadsManager.getDownloads()); + + setOnTouchListener((v, event) -> { + v.requestFocusFromTouch(); + return false; + }); + } + + @Override + public void onDestroy() { + mBinding.downloadsList.removeOnScrollListener(mScrollListener); + } + + @Override + public void onShow() { + mDownloadsManager.addListener(this); + onDownloadsUpdate(mDownloadsManager.getDownloads()); + updateLayout(); + } + + @Override + public void onHide() { + mDownloadsManager.removeListener(this); + } + + private final DownloadItemCallback mDownloadItemCallback = new DownloadItemCallback() { + + @Override + public void onClick(@NonNull View view, @NonNull Download item) { + mBinding.downloadsList.requestFocusFromTouch(); + + SessionStore.get().getActiveSession().loadUri(item.getOutputFile()); + + WindowWidget window = mWidgetManager.getFocusedWindow(); + window.hidePanel(Windows.PanelType.HISTORY); + } + + @Override + public void onDelete(@NonNull View view, @NonNull Download item) { + mWidgetManager.getFocusedWindow().showConfirmPrompt( + getContext().getString(R.string.download_delete_file_confirm_title), + getContext().getString(R.string.download_delete_file_confirm_body), + new String[]{ + getContext().getString(R.string.download_delete_confirm_cancel), + getContext().getString(R.string.download_delete_confirm_delete) + }, + getContext().getString(R.string.download_delete_file_confirm_checkbox), + index -> { + if (index == PromptDialogWidget.POSITIVE) { + mDownloadsManager.removeDownload(item.getId()); + } + } + ); + } + + @Override + public void onMore(@NonNull View view, @NonNull Download item) { + mBinding.downloadsList.requestFocusFromTouch(); + + int rowPosition = mDownloadsAdapter.getItemPosition(item.getId()); + RecyclerView.ViewHolder row = mBinding.downloadsList.findViewHolderForLayoutPosition(rowPosition); + boolean isLastVisibleItem = false; + if (mBinding.downloadsList.getLayoutManager() instanceof LinearLayoutManager) { + LinearLayoutManager layoutManager = (LinearLayoutManager) mBinding.downloadsList.getLayoutManager(); + int lastItem = mDownloadsAdapter.getItemCount(); + if ((rowPosition == layoutManager.findLastVisibleItemPosition() || rowPosition == layoutManager.findLastCompletelyVisibleItemPosition() || + rowPosition == layoutManager.findLastVisibleItemPosition()-1 || rowPosition == layoutManager.findLastCompletelyVisibleItemPosition()-1) + && rowPosition != lastItem) { + isLastVisibleItem = true; + } + } + + if (row != null) { + mBinding.getCallback().onShowContextMenu( + row.itemView, + item, + isLastVisibleItem); + } + } + }; + + private DownloadsCallback mDownloadsCallback = new DownloadsCallback() { + @Override + public void onDeleteDownloads(@NonNull View view) { + view.requestFocusFromTouch(); + mWidgetManager.getFocusedWindow().showConfirmPrompt( + getContext().getString(R.string.download_delete_all_confirm_title), + getContext().getString(R.string.download_delete_all_confirm_body), + new String[]{ + getContext().getString(R.string.download_delete_confirm_cancel), + getContext().getString(R.string.download_delete_confirm_delete) + }, + getContext().getString(R.string.download_delete_all_confirm_checkbox), + index -> { + if (index == PromptDialogWidget.POSITIVE) { + mDownloadsManager.clearAllDownloads(); + } + } + ); + hideContextMenu(); + } + + @Override + public void onShowContextMenu(@NonNull View view, Download item, boolean isLastVisibleItem) { + showContextMenu( + view, + new DownloadsContextMenuWidget(getContext(), + new DownloadsContextMenuWidget.DownloadsContextMenuItem( + item.getOutputFile(), + item.getTitle(), + item.getId()), + mWidgetManager.canOpenNewWindow()), + mCallback, + isLastVisibleItem); + } + + @Override + public void onHideContextMenu(@NonNull View view) { + hideContextMenu(); + } + + @Override + public void onShowSortingContextMenu(@NonNull View view) { + showSortingContextMenu(view); + } + }; + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + updateLayout(); + } + + @Override + protected void updateLayout() { + post(() -> { + double width = Math.ceil(getWidth()/getContext().getResources().getDisplayMetrics().density); + boolean isNarrow = width < SettingsStore.WINDOW_WIDTH_DEFAULT; + + if (isNarrow != mViewModel.getIsNarrow().getValue().get()) { + mDownloadsAdapter.setNarrow(isNarrow); + + mViewModel.setIsNarrow(isNarrow); + mBinding.executePendingBindings(); + + mViewModel.setIsNarrow(isNarrow); + mBinding.executePendingBindings(); + + requestLayout(); + } + }); + } + + private DownloadsContextMenuCallback mCallback = new DownloadsContextMenuCallback() { + + @Override + public void onOpenInNewWindowClick(LibraryContextMenuWidget.LibraryContextMenuItem item) { + mWidgetManager.openNewWindow(item.getUrl()); + hideContextMenu(); + } + + @Override + public void onOpenInNewTabClick(LibraryContextMenuWidget.LibraryContextMenuItem item) { + mWidgetManager.openNewTabForeground(item.getUrl()); + GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.DOWNLOADS); + hideContextMenu(); + } + + @Override + public void onDelete(DownloadsContextMenuWidget.DownloadsContextMenuItem item) { + mWidgetManager.getFocusedWindow().showConfirmPrompt( + getContext().getString(R.string.download_delete_file_confirm_title), + getContext().getString(R.string.download_delete_file_confirm_body), + new String[]{ + getContext().getString(R.string.download_delete_confirm_cancel), + getContext().getString(R.string.download_delete_confirm_delete) + }, + getContext().getString(R.string.download_delete_file_confirm_checkbox), + index -> { + if (index == PromptDialogWidget.POSITIVE) { + mDownloadsManager.removeDownload(item.getDownloadsId()); + } + } + ); + hideContextMenu(); + } + }; + + protected void showSortingContextMenu(@NonNull View view) { + view.requestFocusFromTouch(); + + hideContextMenu(); + + WindowWidget window = mWidgetManager.getFocusedWindow(); + + float ratio = WidgetPlacement.viewToWidgetRatio(getContext(), window); + + Rect offsetViewBounds = new Rect(); + getDrawingRect(offsetViewBounds); + offsetDescendantRectToMyCoords(view, offsetViewBounds); + + SortingContextMenuWidget menu = new SortingContextMenuWidget(getContext()); + menu.setItemDelegate(item -> { + switch (item) { + case SortingContextMenuWidget.SORT_FILENAME_AZ: + mSortingComparator = mAZFileNameComparator; + break; + case SortingContextMenuWidget.SORT_FILENAME_ZA: + mSortingComparator = mZAFilenameComparator; + break; + case SortingContextMenuWidget.SORT_DATE_ASC: + mSortingComparator = mDownloadDateAscComparator; + break; + case SortingContextMenuWidget.SORT_DATE_DESC: + mSortingComparator = mDownloadDateDescComparator; + break; + case SortingContextMenuWidget.SORT_SIZE_ASC: + mSortingComparator = mDownloadSizeAscComparator; + break; + case SortingContextMenuWidget.SORT_SIZE_DESC: + mSortingComparator = mDownloadSizeDescComparator; + break; + } + onDownloadsUpdate(mDownloadsManager.getDownloads()); + }); + menu.getPlacement().parentHandle = window.getHandle(); + + menu.getPlacement().anchorY = 1.0f; + PointF position = new PointF( + (offsetViewBounds.left + view.getWidth()) * ratio, + -(offsetViewBounds.top + view.getHeight()) * ratio); + menu.getPlacement().translationX = position.x - (menu.getWidth()/menu.getPlacement().density); + menu.getPlacement().translationY = position.y + getResources().getDimension(R.dimen.library_menu_top_margin)/menu.getPlacement().density; + menu.show(UIWidget.REQUEST_FOCUS); + } + + // DownloadsManager.DownloadsListener + + private Comparator mAZFileNameComparator = (o1, o2) -> o1.getFilename().compareTo(o2.getFilename()); + private Comparator mZAFilenameComparator = (o1, o2) -> o2.getFilename().compareTo(o1.getFilename()); + private Comparator mDownloadDateAscComparator = (o1, o2) -> (int)(o1.getLastModified() - o2.getLastModified()); + private Comparator mDownloadDateDescComparator = (o1, o2) -> (int)(o2.getLastModified() - o1.getLastModified()); + private Comparator mDownloadSizeAscComparator = (o1, o2) -> (int)(o1.getSizeBytes() - o2.getSizeBytes()); + private Comparator mDownloadSizeDescComparator = (o1, o2) -> (int)(o2.getSizeBytes() - o1.getSizeBytes()); + + @Override + public void onDownloadsUpdate(@NonNull List downloads) { + if (downloads.size() == 0) { + mViewModel.setIsEmpty(true); + mViewModel.setIsLoading(false); + + } else { + mViewModel.setIsEmpty(false); + mViewModel.setIsLoading(false); + List sorted = downloads.stream().sorted(mSortingComparator).collect(Collectors.toList()); + mDownloadsAdapter.setDownloadsList(sorted); + } + + mBinding.executePendingBindings(); + } + + @Override + public void onDownloadError(@NonNull String error, @NonNull String filename) { + Log.e(LOGTAG, error); + mWidgetManager.getFocusedWindow().showAlert( + getContext().getString(R.string.download_error_title, filename), + error, + null + ); + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HistoryView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/library/HistoryView.java similarity index 76% rename from app/src/common/shared/org/mozilla/vrbrowser/ui/views/HistoryView.java rename to app/src/common/shared/org/mozilla/vrbrowser/ui/views/library/HistoryView.java index daf8f300d..e3a923bfe 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/HistoryView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/library/HistoryView.java @@ -3,7 +3,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.vrbrowser.ui.views; +package org.mozilla.vrbrowser.ui.views.library; import android.annotation.SuppressLint; import android.content.Context; @@ -11,7 +11,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; -import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -19,7 +18,6 @@ import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.RecyclerView.OnScrollListener; import org.mozilla.geckoview.GeckoSessionSettings; import org.mozilla.vrbrowser.R; @@ -34,12 +32,17 @@ import org.mozilla.vrbrowser.telemetry.GleanMetricsService; import org.mozilla.vrbrowser.ui.adapters.HistoryAdapter; import org.mozilla.vrbrowser.ui.callbacks.HistoryCallback; +import org.mozilla.vrbrowser.ui.callbacks.HistoryContextMenuCallback; import org.mozilla.vrbrowser.ui.callbacks.HistoryItemCallback; import org.mozilla.vrbrowser.ui.viewmodel.HistoryViewModel; -import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; +import org.mozilla.vrbrowser.ui.widgets.UIWidget; +import org.mozilla.vrbrowser.ui.widgets.WindowWidget; +import org.mozilla.vrbrowser.ui.widgets.Windows; +import org.mozilla.vrbrowser.ui.widgets.dialogs.ClearHistoryDialogWidget; +import org.mozilla.vrbrowser.ui.widgets.menus.library.HistoryContextMenuWidget; +import org.mozilla.vrbrowser.ui.widgets.menus.library.LibraryContextMenuWidget; import org.mozilla.vrbrowser.utils.SystemUtils; -import java.util.ArrayList; import java.util.Calendar; import java.util.Comparator; import java.util.GregorianCalendar; @@ -47,7 +50,6 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executor; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -62,43 +64,37 @@ import mozilla.components.service.fxa.sync.SyncReason; import mozilla.components.service.fxa.sync.SyncStatusObserver; -public class HistoryView extends FrameLayout implements HistoryStore.HistoryListener { +import static org.mozilla.vrbrowser.ui.widgets.settings.SettingsView.SettingViewType.FXA; + +public class HistoryView extends LibraryView implements HistoryStore.HistoryListener { private static final String LOGTAG = SystemUtils.createLogtag(HistoryView.class); private static final boolean ACCOUNTS_UI_ENABLED = false; - private HistoryViewModel mViewModel; private HistoryBinding mBinding; private Accounts mAccounts; private HistoryAdapter mHistoryAdapter; - private ArrayList mHistoryViewListeners; - private Executor mUIThreadExecutor; + private ClearHistoryDialogWidget mClearHistoryDialog; + private HistoryViewModel mViewModel; public HistoryView(Context aContext) { super(aContext); - initialize(aContext); + initialize(); } public HistoryView(Context aContext, AttributeSet aAttrs) { super(aContext, aAttrs); - initialize(aContext); + initialize(); } public HistoryView(Context aContext, AttributeSet aAttrs, int aDefStyle) { super(aContext, aAttrs, aDefStyle); - initialize(aContext); + initialize(); } - private void initialize(Context aContext) { - mViewModel = new ViewModelProvider( - (VRBrowserActivity)getContext(), - ViewModelProvider.AndroidViewModelFactory.getInstance(((VRBrowserActivity) getContext()).getApplication())) - .get(HistoryViewModel.class); - - mUIThreadExecutor = ((VRBrowserApplication)getContext().getApplicationContext()).getExecutors().mainThread(); - - mHistoryViewListeners = new ArrayList<>(); + protected void initialize() { + super.initialize(); mAccounts = ((VRBrowserApplication)getContext().getApplicationContext()).getAccounts(); if (ACCOUNTS_UI_ENABLED) { @@ -106,6 +102,11 @@ private void initialize(Context aContext) { mAccounts.addSyncListener(mSyncListener); } + mViewModel = new ViewModelProvider( + (VRBrowserActivity)getContext(), + ViewModelProvider.AndroidViewModelFactory.getInstance(((VRBrowserActivity) getContext()).getApplication())) + .get(HistoryViewModel.class); + SessionStore.get().getHistoryStore().addListener(this); updateUI(); @@ -119,6 +120,8 @@ public void updateUI() { // Inflate this data binding layout mBinding = DataBindingUtil.inflate(inflater, R.layout.history, this, true); + mBinding.setLifecycleOwner((VRBrowserActivity)getContext()); + mBinding.setHistoryViewModel(mViewModel); mBinding.setCallback(mHistoryCallback); mHistoryAdapter = new HistoryAdapter(mHistoryItemCallback, getContext()); mBinding.historyList.setAdapter(mHistoryAdapter); @@ -153,6 +156,7 @@ public void updateUI() { }); } + @Override public void onDestroy() { SessionStore.get().getHistoryStore().removeListener(this); @@ -164,21 +168,11 @@ public void onDestroy() { } } + @Override public void onShow() { updateLayout(); } - private OnScrollListener mScrollListener = new OnScrollListener() { - @Override - public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { - super.onScrolled(recyclerView, dx, dy); - - if (recyclerView.getScrollState() != RecyclerView.SCROLL_STATE_SETTLING) { - recyclerView.requestFocus(); - } - } - }; - private final HistoryItemCallback mHistoryItemCallback = new HistoryItemCallback() { @Override public void onClick(View view, VisitInfo item) { @@ -187,7 +181,8 @@ public void onClick(View view, VisitInfo item) { Session session = SessionStore.get().getActiveSession(); session.loadUri(item.getUrl()); - mHistoryViewListeners.forEach((listener) -> listener.onClickItem(view, item)); + WindowWidget window = mWidgetManager.getFocusedWindow(); + window.hidePanel(Windows.PanelType.HISTORY); } @Override @@ -214,26 +209,32 @@ public void onMore(View view, VisitInfo item) { } } - mBinding.getCallback().onShowContextMenu( - row.itemView, - item, - isLastVisibleItem); + if (row != null) { + mBinding.getCallback().onShowContextMenu( + row.itemView, + item, + isLastVisibleItem); + } } }; private HistoryCallback mHistoryCallback = new HistoryCallback() { @Override public void onClearHistory(@NonNull View view) { - mHistoryViewListeners.forEach((listener) -> listener.onClearHistory(view)); + view.requestFocusFromTouch(); + showClearCacheDialog(); + hideContextMenu(); } @Override public void onSyncHistory(@NonNull View view) { + view.requestFocusFromTouch(); mAccounts.syncNowAsync(SyncReason.User.INSTANCE, false); } @Override public void onFxALogin(@NonNull View view) { + view.requestFocusFromTouch(); if (mAccounts.getAccountStatus() == Accounts.AccountStatus.SIGNED_IN) { mAccounts.logoutAsync(); @@ -246,12 +247,12 @@ public void onFxALogin(@NonNull View view) { } else { mAccounts.setLoginOrigin(Accounts.LoginOrigin.HISTORY); - WidgetManagerDelegate widgetManager = ((VRBrowserActivity) getContext()); - widgetManager.openNewTabForeground(url); - widgetManager.getFocusedWindow().getSession().setUaMode(GeckoSessionSettings.USER_AGENT_MODE_MOBILE); + mWidgetManager.openNewTabForeground(url); + mWidgetManager.getFocusedWindow().getSession().setUaMode(GeckoSessionSettings.USER_AGENT_MODE_MOBILE); GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.FXA_LOGIN); - mHistoryViewListeners.forEach((listener) -> listener.onFxALogin(view)); + WindowWidget window = mWidgetManager.getFocusedWindow(); + window.hidePanel(Windows.PanelType.HISTORY); } }, mUIThreadExecutor).exceptionally(throwable -> { @@ -265,24 +266,36 @@ public void onFxALogin(@NonNull View view) { @Override public void onFxASynSettings(@NonNull View view) { - mHistoryViewListeners.forEach((listener) -> listener.onFxASynSettings(view)); + view.requestFocusFromTouch(); + mWidgetManager.getTray().showSettingsDialog(FXA); } @Override public void onShowContextMenu(@NonNull View view, @NonNull VisitInfo item, boolean isLastVisibleItem) { - mHistoryViewListeners.forEach((listener) -> listener.onShowContextMenu(view, item, isLastVisibleItem)); + SessionStore.get().getBookmarkStore().isBookmarked(item.getUrl()).thenAcceptAsync((isBookmarked -> { + showContextMenu( + view, + new HistoryContextMenuWidget(getContext(), + new HistoryContextMenuWidget.LibraryContextMenuItem( + item.getUrl(), + item.getTitle()), + mWidgetManager.canOpenNewWindow(), + isBookmarked), + mCallback, + isLastVisibleItem); + + }), mUIThreadExecutor).exceptionally(throwable -> { + Log.d(LOGTAG, "Error getting the bookmarked status: " + throwable.getLocalizedMessage()); + throwable.printStackTrace(); + return null; + }); } - }; - public void addHistoryListener(@NonNull HistoryCallback listener) { - if (!mHistoryViewListeners.contains(listener)) { - mHistoryViewListeners.add(listener); + @Override + public void onHideContextMenu(@NonNull View view) { + hideContextMenu(); } - } - - public void removeHistoryListener(@NonNull HistoryCallback listener) { - mHistoryViewListeners.remove(listener); - } + }; private SyncStatusObserver mSyncListener = new SyncStatusObserver() { @Override @@ -404,16 +417,12 @@ private void showHistory(List historyItems) { mViewModel.setIsLoading(false); mHistoryAdapter.setHistoryList(historyItems); } - } - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - - updateLayout(); + mBinding.executePendingBindings(); } - private void updateLayout() { + @Override + protected void updateLayout() { post(() -> { double width = Math.ceil(getWidth()/getContext().getResources().getDisplayMetrics().density); boolean isNarrow = width < SettingsStore.WINDOW_WIDTH_DEFAULT; @@ -432,6 +441,40 @@ private void updateLayout() { }); } + private HistoryContextMenuCallback mCallback = new HistoryContextMenuCallback() { + @Override + public void onOpenInNewWindowClick(LibraryContextMenuWidget.LibraryContextMenuItem item) { + mWidgetManager.openNewWindow(item.getUrl()); + hideContextMenu(); + } + + @Override + public void onOpenInNewTabClick(LibraryContextMenuWidget.LibraryContextMenuItem item) { + mWidgetManager.openNewTabForeground(item.getUrl()); + GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.BOOKMARKS); + hideContextMenu(); + } + + @Override + public void onAddToBookmarks(LibraryContextMenuWidget.LibraryContextMenuItem item) { + SessionStore.get().getBookmarkStore().addBookmark(item.getUrl(), item.getTitle()); + hideContextMenu(); + } + + @Override + public void onRemoveFromBookmarks(LibraryContextMenuWidget.LibraryContextMenuItem item) { + SessionStore.get().getBookmarkStore().deleteBookmarkByURL(item.getUrl()); + hideContextMenu(); + } + }; + + public void showClearCacheDialog() { + if (mClearHistoryDialog == null) { + mClearHistoryDialog = new ClearHistoryDialogWidget(getContext()); + } + mClearHistoryDialog.show(UIWidget.REQUEST_FOCUS); + } + // HistoryStore.HistoryListener @Override diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/views/library/LibraryView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/library/LibraryView.java new file mode 100644 index 000000000..ea3b48d61 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/views/library/LibraryView.java @@ -0,0 +1,126 @@ +package org.mozilla.vrbrowser.ui.views.library; + +import android.content.Context; +import android.graphics.PointF; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.VRBrowserActivity; +import org.mozilla.vrbrowser.VRBrowserApplication; +import org.mozilla.vrbrowser.ui.callbacks.LibraryContextMenuCallback; +import org.mozilla.vrbrowser.ui.widgets.UIWidget; +import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; +import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; +import org.mozilla.vrbrowser.ui.widgets.WindowWidget; +import org.mozilla.vrbrowser.ui.widgets.menus.library.LibraryContextMenuWidget; + +import java.util.concurrent.Executor; + +public abstract class LibraryView extends FrameLayout { + + protected WidgetManagerDelegate mWidgetManager; + protected LibraryContextMenuWidget mContextMenu; + protected Executor mUIThreadExecutor; + + public LibraryView(@NonNull Context context) { + super(context); + } + + public LibraryView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public LibraryView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public LibraryView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + protected void initialize() { + mWidgetManager = ((VRBrowserActivity) getContext()); + mUIThreadExecutor = ((VRBrowserApplication)getContext().getApplicationContext()).getExecutors().mainThread(); + } + + public void updateUI() {}; + + public void onDestroy() {}; + + public void onShow() {} + + public void onHide() {} + + protected abstract void updateLayout(); + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + updateLayout(); + } + + protected RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + + if (recyclerView.getScrollState() != RecyclerView.SCROLL_STATE_SETTLING) { + recyclerView.requestFocus(); + } + } + }; + + protected void showContextMenu(@NonNull View view, + @NonNull LibraryContextMenuWidget menu, + @NonNull LibraryContextMenuCallback delegate, + boolean isLastVisibleItem) { + view.requestFocusFromTouch(); + + hideContextMenu(); + + WindowWidget window = mWidgetManager.getFocusedWindow(); + + float ratio = WidgetPlacement.viewToWidgetRatio(getContext(), window); + + Rect offsetViewBounds = new Rect(); + getDrawingRect(offsetViewBounds); + offsetDescendantRectToMyCoords(view, offsetViewBounds); + + mContextMenu = menu; + mContextMenu.getPlacement().parentHandle = window.getHandle(); + + PointF position; + if (isLastVisibleItem) { + mContextMenu.getPlacement().anchorY = 0.0f; + position = new PointF( + (offsetViewBounds.left + view.getWidth()) * ratio, + -(offsetViewBounds.top) * ratio); + + } else { + mContextMenu.getPlacement().anchorY = 1.0f; + position = new PointF( + (offsetViewBounds.left + view.getWidth()) * ratio, + -(offsetViewBounds.top + view.getHeight()) * ratio); + } + mContextMenu.getPlacement().translationX = position.x - (mContextMenu.getWidth()/mContextMenu.getPlacement().density); + mContextMenu.getPlacement().translationY = position.y + getResources().getDimension(R.dimen.library_menu_top_margin)/mContextMenu.getPlacement().density; + + mContextMenu.setItemDelegate(delegate); + mContextMenu.show(UIWidget.REQUEST_FOCUS); + } + + protected void hideContextMenu() { + if (mContextMenu != null && !mContextMenu.isReleased() + && mContextMenu.isVisible()) { + mContextMenu.hide(UIWidget.REMOVE_WIDGET); + } + } +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/AppServicesProvider.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/AppServicesProvider.java new file mode 100644 index 000000000..2449592e9 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/AppServicesProvider.java @@ -0,0 +1,23 @@ +package org.mozilla.vrbrowser.ui.widgets; + +import org.mozilla.vrbrowser.AppExecutors; +import org.mozilla.vrbrowser.browser.Accounts; +import org.mozilla.vrbrowser.browser.Places; +import org.mozilla.vrbrowser.browser.Services; +import org.mozilla.vrbrowser.db.AppDatabase; +import org.mozilla.vrbrowser.db.DataRepository; +import org.mozilla.vrbrowser.downloads.DownloadsManager; +import org.mozilla.vrbrowser.utils.BitmapCache; + +public interface AppServicesProvider { + + Services getServices(); + Places getPlaces(); + AppDatabase getDatabase(); + AppExecutors getExecutors(); + DataRepository getRepository(); + BitmapCache getBitmapCache(); + Accounts getAccounts(); + DownloadsManager getDownloadsManager(); + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java index 7104c9878..b705860f0 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/NavigationBarWidget.java @@ -1077,6 +1077,19 @@ public void onHistoryClicked() { } } + @Override + public void onDownloadsClicked() { + if (mAttachedWindow.isResizing()) { + exitResizeMode(ResizeAction.RESTORE_SIZE); + + } else if (mAttachedWindow.isFullScreen()) { + exitFullScreenMode(); + + } else if (mViewModel.getIsInVRVideo().getValue().get()) { + exitVRVideo(); + } + } + private void finishWidgetResize() { mWidgetManager.finishWidgetResize(mAttachedWindow); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayListener.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayListener.java index 5c3ee5e17..beb5140a0 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayListener.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayListener.java @@ -6,4 +6,5 @@ default void onPrivateBrowsingClicked() {} default void onAddWindowClicked() {} default void onHistoryClicked() {} default void onTabsClicked() {} + default void onDownloadsClicked() {} } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayWidget.java index d52893c20..281a10dfb 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/TrayWidget.java @@ -29,6 +29,8 @@ import org.mozilla.vrbrowser.browser.engine.Session; import org.mozilla.vrbrowser.browser.engine.SessionStore; import org.mozilla.vrbrowser.databinding.TrayBinding; +import org.mozilla.vrbrowser.downloads.Download; +import org.mozilla.vrbrowser.downloads.DownloadsManager; import org.mozilla.vrbrowser.ui.viewmodel.TrayViewModel; import org.mozilla.vrbrowser.ui.viewmodel.WindowViewModel; import org.mozilla.vrbrowser.ui.views.UIButton; @@ -40,13 +42,14 @@ import java.util.Arrays; import java.util.List; -public class TrayWidget extends UIWidget implements WidgetManagerDelegate.UpdateListener { +public class TrayWidget extends UIWidget implements WidgetManagerDelegate.UpdateListener, DownloadsManager.DownloadsListener { private static final int ICON_ANIMATION_DURATION = 200; private static final int TAB_ADDED_NOTIFICATION_ID = 0; private static final int TAB_SENT_NOTIFICATION_ID = 1; private static final int BOOKMARK_ADDED_NOTIFICATION_ID = 2; + private static final int DOWNLOAD_COMPLETED_NOTIFICATION_ID = 3; private WindowViewModel mViewModel; private TrayViewModel mTrayViewModel; @@ -75,6 +78,9 @@ public TrayWidget(Context aContext, AttributeSet aAttrs, int aDefStyle) { } private void initialize(Context aContext) { + // Downloads icon progress clipping doesn't work if HW acceleration is enabled. + setIsHardwareAccelerationEnabled(false); + mTrayViewModel = new ViewModelProvider( (VRBrowserActivity)getContext(), ViewModelProvider.AndroidViewModelFactory.getInstance(((VRBrowserActivity) getContext()).getApplication())) @@ -91,6 +97,7 @@ private void initialize(Context aContext) { mAudio = AudioEngine.fromContext(aContext); mWidgetManager.addUpdateListener(this); + mWidgetManager.getServicesProvider().getDownloadsManager().addListener(this); } public void updateUI() { @@ -172,6 +179,17 @@ public void updateUI() { notifyAddWindowClicked(); }); mBinding.addwindowButton.setCurvedTooltip(false); + + mBinding.downloadsButton.setOnHoverListener(mButtonScaleHoverListener); + mBinding.downloadsButton.setOnClickListener(view -> { + if (mAudio != null) { + mAudio.playSound(AudioEngine.Sound.CLICK); + } + + notifyDownloadsClicked(); + view.requestFocusFromTouch(); + }); + mBinding.downloadsButton.setCurvedTooltip(false); } Observer mIsVisibleObserver = aVisible -> { @@ -287,6 +305,11 @@ private void notifyAddWindowClicked() { mTrayListeners.forEach(TrayListener::onAddWindowClicked); } + private void notifyDownloadsClicked() { + hideNotifications(); + mTrayListeners.forEach(TrayListener::onDownloadsClicked); + } + @Override protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { Context context = getContext(); @@ -311,6 +334,7 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { @Override public void releaseWidget() { mWidgetManager.removeUpdateListener(this); + mWidgetManager.getServicesProvider().getDownloadsManager().removeListener(this); mTrayListeners.clear(); if (mTrayViewModel != null) { @@ -358,6 +382,7 @@ public void detachFromWindow() { if (mViewModel != null) { mViewModel.getIsBookmarksVisible().removeObserver(mIsBookmarksVisible); mViewModel.getIsHistoryVisible().removeObserver(mIsHistoryVisible); + mViewModel.getIsDownloadsVisible().removeObserver(mIsDownloadsVisible); mViewModel = null; } } @@ -379,6 +404,7 @@ public void attachToWindow(@NonNull WindowWidget aWindow) { .get(String.valueOf(mAttachedWindow.hashCode()), WindowViewModel.class); mViewModel.getIsBookmarksVisible().observe((VRBrowserActivity)getContext(), mIsBookmarksVisible); mViewModel.getIsHistoryVisible().observe((VRBrowserActivity)getContext(), mIsHistoryVisible); + mViewModel.getIsDownloadsVisible().observe((VRBrowserActivity)getContext(), mIsDownloadsVisible); mBinding.setViewmodel(mViewModel); @@ -401,6 +427,14 @@ public void attachToWindow(@NonNull WindowWidget aWindow) { } }; + private Observer mIsDownloadsVisible = aBoolean -> { + if (aBoolean.get()) { + animateViewPadding(mBinding.downloadsButton, mMaxPadding, mMinPadding, ICON_ANIMATION_DURATION); + } else { + animateViewPadding(mBinding.downloadsButton, mMinPadding, mMaxPadding, ICON_ANIMATION_DURATION); + } + }; + public void toggleSettingsDialog() { toggleSettingsDialog(SettingsView.SettingViewType.MAIN); } @@ -455,11 +489,21 @@ public void showBookmarkAddedNotification() { showNotification(BOOKMARK_ADDED_NOTIFICATION_ID, mBinding.bookmarksButton, R.string.bookmarks_saved_notification); } + public void showDownloadCompletedNotification(String filename) { + showNotification(DOWNLOAD_COMPLETED_NOTIFICATION_ID, + mBinding.downloadsButton, + getContext().getString(R.string.download_completed_notification, filename)); + } + private void showNotification(int notificationId, UIButton button, int stringRes) { + showNotification(notificationId, button, getContext().getString(stringRes)); + } + + private void showNotification(int notificationId, UIButton button, String string) { NotificationManager.Notification notification = new NotificationManager.Builder(this) .withView(button) .withDensity(R.dimen.tray_tooltip_density) - .withString(stringRes) + .withString(string) .withPosition(NotificationManager.Notification.TOP) .withZTranslation(25.0f).build(); NotificationManager.show(notificationId, notification); @@ -480,4 +524,26 @@ public void onBookmarkAdded() { mWidgetManager.getWindows().showBookmarkAddedNotification(); } }; + + // DownloadsManager.DownloadsListener + + @Override + public void onDownloadsUpdate(@NonNull List downloads) { + long inProgressNum = downloads.stream().filter(item -> item.getStatus() == Download.RUNNING).count(); + mTrayViewModel.setDownloadsNumber((int)inProgressNum); + if (inProgressNum == 0) { + mBinding.downloadsButton.setLevel(0); + + } else { + long size = downloads.stream().filter(item -> item.getStatus() == Download.RUNNING).mapToLong(Download::getSizeBytes).sum(); + long downloaded = downloads.stream().filter(item -> item.getStatus() == Download.RUNNING).mapToLong(Download::getDownloadedBytes).sum(); + long percent = downloaded*100/size; + mBinding.downloadsButton.setLevel((int)percent*100); + } + } + + @Override + public void onDownloadCompleted(@NonNull Download download) { + showDownloadCompletedNotification(download.getFilename()); + } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java index 9a9949ea2..2f9f68f38 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WidgetManagerDelegate.java @@ -90,4 +90,6 @@ interface WorldClickListener { void removeConnectivityListener(ConnectivityReceiver.Delegate aListener); void saveState(); void updateLocale(@NonNull Context context); + @NonNull + AppServicesProvider getServicesProvider(); } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java index e2a1737a1..df1031371 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/WindowWidget.java @@ -5,12 +5,13 @@ package org.mozilla.vrbrowser.ui.widgets; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.SharedPreferences; +import android.content.Intent; import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Matrix; -import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.SurfaceTexture; @@ -30,6 +31,7 @@ import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.UiThread; +import androidx.core.content.FileProvider; import androidx.lifecycle.ViewModelProvider; import org.mozilla.geckoview.AllowOrDeny; @@ -49,24 +51,23 @@ import org.mozilla.vrbrowser.browser.engine.Session; import org.mozilla.vrbrowser.browser.engine.SessionState; import org.mozilla.vrbrowser.browser.engine.SessionStore; +import org.mozilla.vrbrowser.downloads.DownloadJob; +import org.mozilla.vrbrowser.downloads.DownloadsManager; import org.mozilla.vrbrowser.telemetry.GleanMetricsService; import org.mozilla.vrbrowser.telemetry.TelemetryWrapper; -import org.mozilla.vrbrowser.ui.adapters.Bookmark; -import org.mozilla.vrbrowser.ui.callbacks.BookmarksCallback; -import org.mozilla.vrbrowser.ui.callbacks.HistoryCallback; -import org.mozilla.vrbrowser.ui.callbacks.LibraryItemContextMenuClickCallback; import org.mozilla.vrbrowser.ui.viewmodel.WindowViewModel; -import org.mozilla.vrbrowser.ui.views.BookmarksView; -import org.mozilla.vrbrowser.ui.views.HistoryView; -import org.mozilla.vrbrowser.ui.widgets.dialogs.ClearHistoryDialogWidget; +import org.mozilla.vrbrowser.ui.views.library.BookmarksView; +import org.mozilla.vrbrowser.ui.views.library.DownloadsView; +import org.mozilla.vrbrowser.ui.views.library.HistoryView; +import org.mozilla.vrbrowser.ui.views.library.LibraryView; import org.mozilla.vrbrowser.ui.widgets.dialogs.PromptDialogWidget; import org.mozilla.vrbrowser.ui.widgets.dialogs.SelectionActionWidget; import org.mozilla.vrbrowser.ui.widgets.menus.ContextMenuWidget; -import org.mozilla.vrbrowser.ui.widgets.menus.LibraryMenuWidget; import org.mozilla.vrbrowser.utils.StringUtils; import org.mozilla.vrbrowser.utils.UrlUtils; import org.mozilla.vrbrowser.utils.ViewUtils; +import java.io.File; import java.util.Arrays; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -77,10 +78,8 @@ import mozilla.components.concept.storage.PageObservation; import mozilla.components.concept.storage.PageVisit; import mozilla.components.concept.storage.RedirectSource; -import mozilla.components.concept.storage.VisitInfo; import mozilla.components.concept.storage.VisitType; -import static org.mozilla.vrbrowser.ui.widgets.settings.SettingsView.SettingViewType.FXA; import static org.mozilla.vrbrowser.utils.ServoUtils.isInstanceOfServoSession; public class WindowWidget extends UIWidget implements SessionChangeListener, @@ -106,10 +105,8 @@ public class WindowWidget extends UIWidget implements SessionChangeListener, private PromptDialogWidget mAlertDialog; private PromptDialogWidget mConfirmDialog; private PromptDialogWidget mAppDialog; - private ClearHistoryDialogWidget mClearHistoryDialog; private ContextMenuWidget mContextMenu; private SelectionActionWidget mSelectionMenu; - private LibraryMenuWidget mLibraryItemContextMenu; private int mWidthBackup; private int mHeightBackup; private int mBorderWidth; @@ -120,6 +117,7 @@ public class WindowWidget extends UIWidget implements SessionChangeListener, private int mWindowId; private BookmarksView mBookmarksView; private HistoryView mHistoryView; + private DownloadsView mDownloadsView; private Windows.WindowPlacement mWindowPlacement = Windows.WindowPlacement.FRONT; private Windows.WindowPlacement mWindowPlacementBeforeFullscreen = Windows.WindowPlacement.FRONT; private float mMaxWindowScale = 3; @@ -128,8 +126,6 @@ public class WindowWidget extends UIWidget implements SessionChangeListener, boolean mActive = false; boolean mHovered = false; boolean mClickedAfterFocus = false; - boolean mIsBookmarksVisible = false; - boolean mIsHistoryVisible = false; private WidgetPlacement mPlacementBeforeFullscreen; private WidgetPlacement mPlacementBeforeResize; private boolean mIsResizing; @@ -140,6 +136,8 @@ public class WindowWidget extends UIWidget implements SessionChangeListener, private WindowViewModel mViewModel; private CopyOnWriteArrayList mSetViewQueuedCalls; private SharedPreferences mPrefs; + private DownloadsManager mDownloadsManager; + private Windows.PanelType mVisiblePanelType; public interface WindowListener { default void onFocusRequest(@NonNull WindowWidget aWindow) {} @@ -181,7 +179,9 @@ private void initialize(Context aContext) { mWidgetManager = (WidgetManagerDelegate) aContext; mBorderWidth = SettingsStore.getInstance(aContext).getTransparentBorderWidth(); - // ModelView creation and observers setup + mDownloadsManager = mWidgetManager.getServicesProvider().getDownloadsManager(); + + // ModelView creation and observers setup mViewModel = new ViewModelProvider( (VRBrowserActivity)getContext(), ViewModelProvider.AndroidViewModelFactory.getInstance(((VRBrowserActivity) getContext()).getApplication())) @@ -195,10 +195,8 @@ private void initialize(Context aContext) { setupListeners(mSession); mBookmarksView = new BookmarksView(aContext); - mBookmarksView.addBookmarksListener(mBookmarksViewListener); - mHistoryView = new HistoryView(aContext); - mHistoryView.addHistoryListener(mHistoryListener); + mDownloadsView = new DownloadsView(aContext); SessionStore.get().getBookmarkStore().addListener(mBookmarksListener); @@ -306,11 +304,14 @@ public void hide(@HideFlags int aHideFlag) { @Override protected void onDismiss() { - if (isBookmarksVisible()) { - hideBookmarks(); + if (mViewModel.getIsBookmarksVisible().getValue().get()) { + hidePanel(Windows.PanelType.BOOKMARKS); + + } else if (mViewModel.getIsHistoryVisible().getValue().get()) { + hidePanel(Windows.PanelType.HISTORY); - } else if (isHistoryVisible()) { - hideHistory(); + } else if (mViewModel.getIsDownloadsVisible().getValue().get()) { + hidePanel(Windows.PanelType.DOWNLOADS); } else { if (mSession.canGoBack()) { @@ -344,6 +345,7 @@ public void onConfigurationChanged(Configuration newConfig) { mHistoryView.updateUI(); mBookmarksView.updateUI(); + mDownloadsView.updateUI(); mViewModel.refresh(); } @@ -355,6 +357,7 @@ public void close() { releaseWidget(); mBookmarksView.onDestroy(); mHistoryView.onDestroy(); + mDownloadsView.onDestroy(); mViewModel.setIsTopBarVisible(false); mViewModel.setIsTitleBarVisible(false); SessionStore.get().destroySession(mSession); @@ -450,11 +453,15 @@ private void unsetView(View view, boolean switchSurface) { } public boolean isBookmarksVisible() { - return (mView != null && mView == mBookmarksView); + return mViewModel.getIsBookmarksVisible().getValue().get(); } public boolean isHistoryVisible() { - return (mView != null && mView == mHistoryView); + return mViewModel.getIsHistoryVisible().getValue().get(); + } + + public boolean isDownloadsVisible() { + return mViewModel.getIsDownloadsVisible().getValue().get(); } public int getWindowWidth() { @@ -465,87 +472,125 @@ public int getWindowHeight() { return mWidgetPlacement.height; } - public void switchBookmarks() { - if (isHistoryVisible()) { - hideHistory(false); - showBookmarks(false); + private void hideLibraryPanels() { + if (mViewModel.getIsBookmarksVisible().getValue().get()) { + hidePanel(Windows.PanelType.BOOKMARKS); - } else if (isBookmarksVisible()) { - hideBookmarks(); + } else if (mViewModel.getIsHistoryVisible().getValue().get()) { + hidePanel(Windows.PanelType.HISTORY); - } else { - showBookmarks(); + } else if (mViewModel.getIsDownloadsVisible().getValue().get()) { + hidePanel(Windows.PanelType.DOWNLOADS); } } - private void showBookmarks() { - showBookmarks(true); - } + public void switchPanel(@NonNull Windows.PanelType panelType) { + switch (panelType) { + case BOOKMARKS: + if (mViewModel.getIsHistoryVisible().getValue().get() || + mViewModel.getIsDownloadsVisible().getValue().get()) { + hidePanel(Windows.PanelType.HISTORY, false); + hidePanel(Windows.PanelType.DOWNLOADS, false); + showPanel(Windows.PanelType.BOOKMARKS, false); - private void showBookmarks(boolean switchSurface) { - if (mView == null) { - setView(mBookmarksView, switchSurface); - mBookmarksView.onShow(); - mViewModel.setIsBookmarksVisible(true); - mIsBookmarksVisible = true; - } - } + } else if (mViewModel.getIsBookmarksVisible().getValue().get()) { + hidePanel(Windows.PanelType.BOOKMARKS); - public void hideBookmarks() { - hideBookmarks(true); - } - - private void hideBookmarks(boolean switchSurface) { - if (mView != null) { - unsetView(mBookmarksView, switchSurface); - mViewModel.setIsBookmarksVisible(false); - mIsBookmarksVisible = false; + } else { + showPanel(Windows.PanelType.BOOKMARKS); + } + break; + case HISTORY: + if (mViewModel.getIsBookmarksVisible().getValue().get() || + mViewModel.getIsDownloadsVisible().getValue().get()) { + hidePanel(Windows.PanelType.BOOKMARKS, false); + hidePanel(Windows.PanelType.DOWNLOADS, false); + showPanel(Windows.PanelType.HISTORY, false); + + } else if (mViewModel.getIsHistoryVisible().getValue().get()) { + hidePanel(Windows.PanelType.HISTORY); + + } else { + showPanel(Windows.PanelType.HISTORY); + } + break; + case DOWNLOADS: + if (mViewModel.getIsBookmarksVisible().getValue().get() || + mViewModel.getIsHistoryVisible().getValue().get()) { + hidePanel(Windows.PanelType.BOOKMARKS, false); + hidePanel(Windows.PanelType.HISTORY, false); + showPanel(Windows.PanelType.DOWNLOADS, false); + + } else if (mViewModel.getIsDownloadsVisible().getValue().get()) { + hidePanel(Windows.PanelType.DOWNLOADS); + + } else { + showPanel(Windows.PanelType.DOWNLOADS); + } + break; + case NONE: + break; } } - public void switchHistory() { - if (isBookmarksVisible()) { - hideBookmarks(false); - showHistory(false); - - } else if (isHistoryVisible()) { - hideHistory(); - - } else { - showHistory(); - } + private void showPanel(@NonNull Windows.PanelType panelType) { + showPanel(panelType, true); } - private void hideLibraryPanels() { - if (isBookmarksVisible()) { - hideBookmarks(); - } else if (isHistoryVisible()) { - hideHistory(); + Runnable mRestoreFirstPaint; + + private void showPanel(@NonNull Windows.PanelType panelType, boolean switchSurface) { + LibraryView libraryView = getPanelByType(panelType); + if (mView == null && libraryView != null) { + setView(libraryView, switchSurface); + libraryView.onShow(); + mViewModel.setIsPanelVisible(panelType, true); + if (mRestoreFirstPaint == null && !isFirstPaintReady() && (mFirstDrawCallback != null)) { + onFirstContentfulPaint(mSession.getGeckoSession()); + mRestoreFirstPaint = () -> { + setFirstPaintReady(false); + setFirstDrawCallback(() -> { + setFirstPaintReady(true); + if (mWidgetManager != null) { + mWidgetManager.updateWidget(WindowWidget.this); + } + }); + if (mWidgetManager != null) { + mWidgetManager.updateWidget(WindowWidget.this); + } + }; + } } } - private void showHistory() { - showHistory(true); + public void hidePanel(@NonNull Windows.PanelType panelType) { + hidePanel(panelType, true); } - private void showHistory(boolean switchSurface) { - if (mView == null) { - setView(mHistoryView, switchSurface); - mHistoryView.onShow(); - mViewModel.setIsHistoryVisible(true); - mIsHistoryVisible = true; + public void hidePanel(@NonNull Windows.PanelType panelType, boolean switchSurface) { + LibraryView libraryView = getPanelByType(panelType); + if (mView != null && libraryView != null) { + unsetView(libraryView, switchSurface); + libraryView.onHide(); + mViewModel.setIsPanelVisible(panelType, false); + } + if (switchSurface && mRestoreFirstPaint != null) { + mUIThreadExecutor.execute(mRestoreFirstPaint); + mRestoreFirstPaint = null; } } - public void hideHistory() { - hideHistory(true); - } - - public void hideHistory(boolean switchSurface) { - if (mView != null) { - unsetView(mHistoryView, switchSurface); - mViewModel.setIsHistoryVisible(false); - mIsHistoryVisible = false; + @Nullable + private LibraryView getPanelByType(@NonNull Windows.PanelType panelType) { + switch (panelType) { + case BOOKMARKS: + return mBookmarksView; + case HISTORY: + return mHistoryView; + case DOWNLOADS: + return mDownloadsView; + default: + return null; } } @@ -969,8 +1014,6 @@ public void releaseWidget() { mTexture.release(); mTexture = null; } - mBookmarksView.removeBookmarksListener(mBookmarksViewListener); - mHistoryView.removeHistoryListener(mHistoryListener); mWidgetManager.getNavigationBar().removeNavigationBarListener(mNavigationBarListener); SessionStore.get().getBookmarkStore().removeListener(mBookmarksListener); mPromptDelegate.detachFromWindow(); @@ -1020,17 +1063,19 @@ public void setVisible(boolean aVisible) { } mWidgetPlacement.visible = aVisible; if (!aVisible) { - if (mIsBookmarksVisible || mIsHistoryVisible) { + if (mViewModel.getIsHistoryVisible().getValue().get() || + mViewModel.getIsBookmarksVisible().getValue().get() || + mViewModel.getIsDownloadsVisible().getValue().get()) { mWidgetManager.popWorldBrightness(this); } } else { - if (mIsBookmarksVisible || mIsHistoryVisible) { + if (mViewModel.getIsHistoryVisible().getValue().get() || + mViewModel.getIsBookmarksVisible().getValue().get() || + mViewModel.getIsDownloadsVisible().getValue().get()) { mWidgetManager.pushWorldBrightness(this, WidgetManagerDelegate.DEFAULT_DIM_BRIGHTNESS); } } - mIsBookmarksVisible = isBookmarksVisible(); - mIsHistoryVisible = isHistoryVisible(); mWidgetManager.updateWidget(this); if (!aVisible) { clearFocus(); @@ -1251,6 +1296,35 @@ public void showConfirmPrompt(String title, @NonNull String msg, @NonNull String mConfirmDialog.show(REQUEST_FOCUS); } + public void showConfirmPrompt(@NonNull String title, + @NonNull String msg, + @NonNull String[] btnMsg, + @NonNull String checkBoxText, + @Nullable PromptDialogWidget.Delegate callback) { + if (mConfirmDialog == null) { + mConfirmDialog = new PromptDialogWidget(getContext()); + mConfirmDialog.setButtons(new int[] { + R.string.cancel_button, + R.string.ok_button + }); + mConfirmDialog.setCheckboxVisible(true); + mConfirmDialog.setCheckboxText(checkBoxText); + mConfirmDialog.setDescriptionVisible(false); + } + mConfirmDialog.setTitle(title); + mConfirmDialog.setBody(msg); + mConfirmDialog.setButtons(btnMsg); + mConfirmDialog.setButtonsDelegate(index -> { + mConfirmDialog.hide(REMOVE_WIDGET); + if (callback != null) { + callback.onButtonClicked(index); + } + mConfirmDialog.releaseWidget(); + mConfirmDialog = null; + }); + mConfirmDialog.show(REQUEST_FOCUS); + } + public void showDialog(@NonNull String title, @NonNull @StringRes int description, @NonNull @StringRes int [] btnMsg, @Nullable PromptDialogWidget.Delegate buttonsCallback, @Nullable Runnable linkCallback) { mAppDialog = new PromptDialogWidget(getContext()); @@ -1278,13 +1352,6 @@ public void showDialog(@NonNull String title, @NonNull @StringRes int descripti mAppDialog.show(REQUEST_FOCUS); } - public void showClearCacheDialog() { - if (mClearHistoryDialog == null) { - mClearHistoryDialog = new ClearHistoryDialogWidget(getContext()); - } - mClearHistoryDialog.show(REQUEST_FOCUS); - } - public void showFirstTimeDrmDialog(@NonNull Runnable callback) { showConfirmPrompt( getContext().getString(R.string.drm_first_use_title), @@ -1344,148 +1411,6 @@ private int getWindowWidth(float aWorldWidth) { return (int) Math.floor(SettingsStore.WINDOW_WIDTH_DEFAULT * aWorldWidth / WidgetPlacement.floatDimension(getContext(), R.dimen.window_world_width)); } - private void showLibraryItemContextMenu(@NonNull View view, LibraryMenuWidget.LibraryContextMenuItem item, boolean isLastVisibleItem) { - view.requestFocusFromTouch(); - - hideContextMenus(); - - float ratio = WidgetPlacement.viewToWidgetRatio(getContext(), WindowWidget.this); - - Rect offsetViewBounds = new Rect(); - getDrawingRect(offsetViewBounds); - offsetDescendantRectToMyCoords(view, offsetViewBounds); - - SessionStore.get().getBookmarkStore().isBookmarked(item.getUrl()).thenAcceptAsync((isBookmarked -> { - mLibraryItemContextMenu = new LibraryMenuWidget(getContext(), item, mWidgetManager.canOpenNewWindow(), isBookmarked); - mLibraryItemContextMenu.getPlacement().parentHandle = getHandle(); - - PointF position; - if (isLastVisibleItem) { - mLibraryItemContextMenu.mWidgetPlacement.anchorY = 0.0f; - position = new PointF( - (offsetViewBounds.left + view.getWidth()) * ratio, - -(offsetViewBounds.top) * ratio); - - } else { - mLibraryItemContextMenu.mWidgetPlacement.anchorY = 1.0f; - position = new PointF( - (offsetViewBounds.left + view.getWidth()) * ratio, - -(offsetViewBounds.top + view.getHeight()) * ratio); - } - mLibraryItemContextMenu.mWidgetPlacement.translationX = position.x - (mLibraryItemContextMenu.getWidth()/mLibraryItemContextMenu.mWidgetPlacement.density); - mLibraryItemContextMenu.mWidgetPlacement.translationY = position.y + getResources().getDimension(R.dimen.library_menu_top_margin)/mLibraryItemContextMenu.mWidgetPlacement.density; - - mLibraryItemContextMenu.setItemDelegate((new LibraryItemContextMenuClickCallback() { - @Override - public void onOpenInNewWindowClick(LibraryMenuWidget.LibraryContextMenuItem item) { - mWidgetManager.openNewWindow(item.getUrl()); - hideContextMenus(); - } - - @Override - public void onOpenInNewTabClick(LibraryMenuWidget.LibraryContextMenuItem item) { - mWidgetManager.openNewTabForeground(item.getUrl()); - if (item.getType() == LibraryMenuWidget.LibraryItemType.HISTORY) { - GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.HISTORY); - } else if (item.getType() == LibraryMenuWidget.LibraryItemType.BOOKMARKS) { - GleanMetricsService.Tabs.openedCounter(GleanMetricsService.Tabs.TabSource.BOOKMARKS); - } - hideContextMenus(); - } - - @Override - public void onAddToBookmarks(LibraryMenuWidget.LibraryContextMenuItem item) { - SessionStore.get().getBookmarkStore().addBookmark(item.getUrl(), item.getTitle()); - hideContextMenus(); - } - - @Override - public void onRemoveFromBookmarks(LibraryMenuWidget.LibraryContextMenuItem item) { - SessionStore.get().getBookmarkStore().deleteBookmarkByURL(item.getUrl()); - hideContextMenus(); - } - })); - mLibraryItemContextMenu.show(REQUEST_FOCUS); - - }), mUIThreadExecutor).exceptionally(throwable -> { - Log.d(LOGTAG, "Error getting the bookmarked status: " + throwable.getLocalizedMessage()); - throwable.printStackTrace(); - return null; - }); - } - - private BookmarksCallback mBookmarksViewListener = new BookmarksCallback() { - @Override - public void onShowContextMenu(@NonNull View view, @NonNull Bookmark item, boolean isLastVisibleItem) { - showLibraryItemContextMenu( - view, - new LibraryMenuWidget.LibraryContextMenuItem( - item.getUrl(), - item.getTitle(), - LibraryMenuWidget.LibraryItemType.BOOKMARKS), - isLastVisibleItem); - } - - @Override - public void onFxASynSettings(@NonNull View view) { - mWidgetManager.getTray().showSettingsDialog(FXA); - } - - @Override - public void onHideContextMenu(@NonNull View view) { - hideContextMenus(); - } - - @Override - public void onFxALogin(@NonNull View view) { - hideBookmarks(); - } - - @Override - public void onClickItem(@NonNull View view, Bookmark item) { - hideBookmarks(); - } - }; - - private HistoryCallback mHistoryListener = new HistoryCallback() { - @Override - public void onClearHistory(@NonNull View view) { - view.requestFocusFromTouch(); - showClearCacheDialog(); - } - - @Override - public void onShowContextMenu(@NonNull View view, @NonNull VisitInfo item, boolean isLastVisibleItem) { - showLibraryItemContextMenu( - view, - new LibraryMenuWidget.LibraryContextMenuItem( - item.getUrl(), - item.getTitle(), - LibraryMenuWidget.LibraryItemType.HISTORY), - isLastVisibleItem); - } - - @Override - public void onFxASynSettings(@NonNull View view) { - mWidgetManager.getTray().showSettingsDialog(FXA); - } - - @Override - public void onHideContextMenu(@NonNull View view) { - hideContextMenus(); - } - - @Override - public void onFxALogin(@NonNull View view) { - hideHistory(); - } - - @Override - public void onClickItem(@NonNull View view, @NonNull VisitInfo item) { - hideHistory(); - } - }; - private NavigationBarWidget.NavigationListener mNavigationBarListener = new NavigationBarWidget.NavigationListener() { @Override public void onBack() { @@ -1565,10 +1490,51 @@ private void hideContextMenus() { mWidgetPlacement.tintColor = 0xFFFFFFFF; mWidgetManager.updateWidget(this); } + } + + public void startDownload(@NonNull DownloadJob downloadJob, boolean showConfirmDialog) { + Runnable download = () -> { + if (showConfirmDialog) { + mWidgetManager.getFocusedWindow().showConfirmPrompt( + getResources().getString(R.string.download_confirm_title), + downloadJob.getFilename(), + new String[]{ + getResources().getString(R.string.download_confirm_cancel), + getResources().getString(R.string.download_confirm_download)}, + index -> { + if (index == PromptDialogWidget.POSITIVE) { + mDownloadsManager.startDownload(downloadJob); + } + } + ); + + } else { + mDownloadsManager.startDownload(downloadJob); + } + }; + @SettingsStore.Storage int storage = SettingsStore.getInstance(getContext()).getDownloadsStorage(); + if (storage == SettingsStore.EXTERNAL) { + mWidgetManager.requestPermission( + downloadJob.getUri(), + android.Manifest.permission.WRITE_EXTERNAL_STORAGE, + new GeckoSession.PermissionDelegate.Callback() { + @Override + public void grant() { + download.run(); + } - if (mLibraryItemContextMenu != null && !mLibraryItemContextMenu.isReleased() - && mLibraryItemContextMenu.isVisible()) { - mLibraryItemContextMenu.hide(REMOVE_WIDGET); + @Override + public void reject() { + mWidgetManager.getFocusedWindow().showAlert( + "Permission error", + "External storage write permission is required to download files to the external storage", + null + ); + } + }); + + } else { + download.run(); } } @@ -1581,10 +1547,6 @@ public void onFullScreen(@NonNull GeckoSession session, boolean aFullScreen) { @Override public void onContextMenu(GeckoSession session, int screenX, int screenY, ContextElement element) { - if (element.type == ContextElement.TYPE_VIDEO) { - return; - } - hideContextMenus(); mContextMenu = new ContextMenuWidget(getContext()); @@ -1628,6 +1590,42 @@ public void onFirstContentfulPaint(@NonNull GeckoSession session) { } } + @Override + public void onExternalResponse(@NonNull GeckoSession geckoSession, @NonNull GeckoSession.WebResponseInfo webResponseInfo) { + // We don't want to trigger downloads of already downloaded files that we can't open + // so we let the system handle it. + if (!UrlUtils.isFileUri(webResponseInfo.uri)) { + DownloadJob job = DownloadJob.from(webResponseInfo); + startDownload(job, true); + + } else { + showConfirmPrompt(getResources().getString(R.string.download_open_file_unsupported_title), + getResources().getString(R.string.download_open_file_unsupported_body), + new String[]{ + getResources().getString(R.string.download_open_file_unsupported_cancel), + getResources().getString(R.string.download_open_file_unsupported_open) + }, index -> { + if (index == PromptDialogWidget.POSITIVE) { + Uri contentUri = FileProvider.getUriForFile( + getContext(), + getContext().getApplicationContext().getPackageName() + ".provider", + new File(webResponseInfo.uri.substring("file://".length()))); + Intent newIntent = new Intent(Intent.ACTION_VIEW); + newIntent.setDataAndType(contentUri, webResponseInfo.contentType); + newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION); + try { + getContext().startActivity(newIntent); + } catch (ActivityNotFoundException ex) { + showAlert( + getResources().getString(R.string.download_open_file_error_title), + getResources().getString(R.string.download_open_file_error_body), + null); + } + } + }); + } + } + // VideoAvailabilityListener private Media mMedia; @@ -1745,10 +1743,13 @@ public void onCanGoForward(@NonNull GeckoSession geckoSession, boolean canGoForw Uri uri = Uri.parse(aRequest.uri); if (UrlUtils.isAboutPage(uri.toString())) { if(UrlUtils.isBookmarksUrl(uri.toString())) { - showBookmarks(); + showPanel(Windows.PanelType.BOOKMARKS); } else if (UrlUtils.isHistoryUrl(uri.toString())) { - showHistory(); + showPanel(Windows.PanelType.HISTORY); + + } else if (UrlUtils.isDownloadsUrl(uri.toString())) { + showPanel(Windows.PanelType.DOWNLOADS); } else { hideLibraryPanels(); diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Windows.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Windows.java index 09f97b0a0..4d4419a59 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Windows.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/Windows.java @@ -87,8 +87,13 @@ public void load(WindowWidget aWindow, WindowsState aState, int aTabIndex) { tabIndex = aTabIndex; if (aWindow.isBookmarksVisible()) { panelType = PanelType.BOOKMARKS; + } else if (aWindow.isHistoryVisible()) { panelType = PanelType.HISTORY; + + } else if (aWindow.isDownloadsVisible()) { + panelType = PanelType.DOWNLOADS; + } else { panelType = PanelType.NONE; } @@ -123,10 +128,11 @@ class WindowsState { private PromptDialogWidget mNoInternetDialog; private boolean mCompositorPaused = false; - private enum PanelType { + public enum PanelType { NONE, BOOKMARKS, - HISTORY + HISTORY, + DOWNLOADS } public enum WindowPlacement{ @@ -296,14 +302,19 @@ private WindowWidget addRestoredWindow(@NonNull WindowState aState, @NonNull Ses newWindow.getPlacement().worldWidth = aState.worldWidth; newWindow.setRestored(true); placeWindow(newWindow, aState.placement); - if (aState.panelType != null) { - switch (aState.panelType) { - case BOOKMARKS: - newWindow.getSession().loadUri(UrlUtils.ABOUT_BOOKMARKS); - break; - case HISTORY: - newWindow.getSession().loadUri(UrlUtils.ABOUT_HISTORY); - break; + if (newWindow.getSession() != null) { + if (aState.panelType != null) { + switch (aState.panelType) { + case BOOKMARKS: + newWindow.getSession().loadUri(UrlUtils.ABOUT_BOOKMARKS); + break; + case HISTORY: + newWindow.getSession().loadUri(UrlUtils.ABOUT_HISTORY); + break; + case DOWNLOADS: + newWindow.getSession().loadUri(UrlUtils.ABOUT_DOWNLOADS); + break; + } } } updateCurvedMode(true); @@ -317,8 +328,9 @@ public void closeWindow(@NonNull WindowWidget aWindow) { WindowWidget leftWindow = getLeftWindow(); WindowWidget rightWindow = getRightWindow(); - aWindow.hideBookmarks(); - aWindow.hideHistory(); + aWindow.hidePanel(PanelType.BOOKMARKS); + aWindow.hidePanel(PanelType.HISTORY); + aWindow.hidePanel(PanelType.DOWNLOADS); if (leftWindow == aWindow) { removeWindow(leftWindow); @@ -954,7 +966,7 @@ public void onAuthenticationProblems() { // Tray Listener @Override public void onBookmarksClicked() { - mFocusedWindow.switchBookmarks(); + mFocusedWindow.switchPanel(PanelType.BOOKMARKS); } @Override @@ -976,7 +988,12 @@ public void onAddWindowClicked() { @Override public void onHistoryClicked() { - mFocusedWindow.switchHistory(); + mFocusedWindow.switchPanel(PanelType.HISTORY); + } + + @Override + public void onDownloadsClicked() { + mFocusedWindow.switchPanel(PanelType.DOWNLOADS); } @Override diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/ContextMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/ContextMenuWidget.java index a28730454..70945a478 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/ContextMenuWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/ContextMenuWidget.java @@ -11,8 +11,11 @@ import android.content.res.Configuration; import android.net.Uri; -import org.mozilla.geckoview.GeckoSession; +import androidx.annotation.StringRes; + +import org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement; import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.downloads.DownloadJob; import org.mozilla.vrbrowser.telemetry.GleanMetricsService; import org.mozilla.vrbrowser.ui.widgets.WidgetManagerDelegate; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; @@ -74,7 +77,7 @@ public void setDismissCallback(Runnable aCallback) { mDismissCallback = aCallback; } - public void setContextElement(GeckoSession.ContentDelegate.ContextElement aContextElement) { + public void setContextElement(ContextElement aContextElement) { mItems = new ArrayList<>(); mItems.add(new MenuWidget.MenuItem(aContextElement.linkUri, 0, null)); final WidgetManagerDelegate widgetManager = mWidgetManager; @@ -93,22 +96,59 @@ public void setContextElement(GeckoSession.ContentDelegate.ContextElement aConte } onDismiss(); })); + if (!StringUtils.isEmpty(aContextElement.linkUri)) { + mItems.add(new MenuWidget.MenuItem(getContext().getString(R.string.context_menu_download_link), 0, () -> { + DownloadJob job = DownloadJob.fromLink(aContextElement); + widgetManager.getFocusedWindow().startDownload(job, false); + // TODO Add Download from context menu Telemetry + onDismiss(); + })); + } + if (!StringUtils.isEmpty(aContextElement.srcUri)) { + @StringRes int srcText; + switch (aContextElement.type) { + case ContextElement.TYPE_IMAGE: + srcText = R.string.context_menu_download_image; + break; + case ContextElement.TYPE_VIDEO: + srcText = R.string.context_menu_download_video; + break; + case ContextElement.TYPE_AUDIO: + srcText = R.string.context_menu_download_audio; + break; + default: + srcText = R.string.context_menu_download_link; + break; + } + mItems.add(new MenuWidget.MenuItem(getContext().getString(srcText), 0, () -> { + DownloadJob job = DownloadJob.fromSrc(aContextElement); + widgetManager.getFocusedWindow().startDownload(job, false); + // TODO Add Download from context menu Telemetry + onDismiss(); + })); + } mItems.add(new MenuWidget.MenuItem(getContext().getString(R.string.context_menu_copy_link), 0, () -> { ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); + Uri uri; if (aContextElement.linkUri != null) { - Uri uri = Uri.parse(aContextElement.linkUri); - if (uri != null) { - String label = aContextElement.title; - if (StringUtils.isEmpty(label)) { - label = aContextElement.altText; - } - if (StringUtils.isEmpty(label)) { - label = aContextElement.altText; - } - if (StringUtils.isEmpty(label)) { - label = aContextElement.linkUri; - } - ClipData clip = ClipData.newRawUri(label, uri); + uri = Uri.parse(aContextElement.linkUri); + + } else { + uri = Uri.parse(aContextElement.srcUri); + } + if (uri != null) { + String label = aContextElement.title; + if (StringUtils.isEmpty(label)) { + label = aContextElement.altText; + } + if (StringUtils.isEmpty(label)) { + label = aContextElement.altText; + } + if (StringUtils.isEmpty(label)) { + label = uri.toString(); + } + ClipData clip = ClipData.newRawUri(label, uri); + if (clipboard != null) { clipboard.setPrimaryClip(clip); } } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/BookmarksContextMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/BookmarksContextMenuWidget.java new file mode 100644 index 000000000..ce10e505d --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/BookmarksContextMenuWidget.java @@ -0,0 +1,11 @@ +package org.mozilla.vrbrowser.ui.widgets.menus.library; + +import android.content.Context; + +public class BookmarksContextMenuWidget extends LibraryContextMenuWidget { + + public BookmarksContextMenuWidget(Context aContext, LibraryContextMenuItem item, boolean canOpenWindows) { + super(aContext, item, canOpenWindows, true); + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/DownloadsContextMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/DownloadsContextMenuWidget.java new file mode 100644 index 000000000..36deb9fcf --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/DownloadsContextMenuWidget.java @@ -0,0 +1,40 @@ +package org.mozilla.vrbrowser.ui.widgets.menus.library; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.ui.callbacks.DownloadsContextMenuCallback; + +public class DownloadsContextMenuWidget extends LibraryContextMenuWidget { + + public DownloadsContextMenuWidget(Context aContext, LibraryContextMenuItem item, boolean canOpenWindows) { + super(aContext, item, canOpenWindows, false); + } + + public static class DownloadsContextMenuItem extends LibraryContextMenuItem { + + long downloadId; + + public DownloadsContextMenuItem(@NonNull String url, String title, long downloadId) { + super(url, title); + + this.downloadId = downloadId; + } + + public long getDownloadsId() { + return downloadId; + } + + } + + protected void setupCustomMenuItems(boolean canOpenWindows, boolean isBookmarked) { + mItems.add(new MenuItem(getContext().getString( + R.string.download_context_delete), + R.drawable.ic_icon_library_clearfromlist, + () -> mItemDelegate.ifPresent((present -> + ((DownloadsContextMenuCallback)mItemDelegate.get()).onDelete((DownloadsContextMenuItem)mItem))))); + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/HistoryContextMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/HistoryContextMenuWidget.java new file mode 100644 index 000000000..199fc0691 --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/HistoryContextMenuWidget.java @@ -0,0 +1,28 @@ +package org.mozilla.vrbrowser.ui.widgets.menus.library; + +import android.content.Context; + +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.ui.callbacks.HistoryContextMenuCallback; + +public class HistoryContextMenuWidget extends LibraryContextMenuWidget { + + public HistoryContextMenuWidget(Context aContext, LibraryContextMenuItem item, boolean canOpenWindows, boolean isBookmarked) { + super(aContext, item, canOpenWindows, isBookmarked); + } + + protected void setupCustomMenuItems(boolean canOpenWindows, boolean isBookmarked) { + mItems.add(new MenuItem(getContext().getString( + isBookmarked ? R.string.history_context_remove_bookmarks : R.string.history_context_add_bookmarks), + isBookmarked ? R.drawable.ic_icon_bookmarked_active : R.drawable.ic_icon_bookmarked, + () -> mItemDelegate.ifPresent((present -> { + if (isBookmarked) { + ((HistoryContextMenuCallback)mItemDelegate.get()).onRemoveFromBookmarks(mItem); + + } else { + ((HistoryContextMenuCallback)mItemDelegate.get()).onAddToBookmarks(mItem); + } + })))); + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/LibraryMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/LibraryContextMenuWidget.java similarity index 70% rename from app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/LibraryMenuWidget.java rename to app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/LibraryContextMenuWidget.java index 7f937b14a..344b566e0 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/LibraryMenuWidget.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/LibraryContextMenuWidget.java @@ -1,4 +1,4 @@ -package org.mozilla.vrbrowser.ui.widgets.menus; +package org.mozilla.vrbrowser.ui.widgets.menus.library; import android.content.Context; import android.content.res.Configuration; @@ -6,30 +6,24 @@ import androidx.annotation.NonNull; import org.mozilla.vrbrowser.R; -import org.mozilla.vrbrowser.ui.callbacks.LibraryItemContextMenuClickCallback; +import org.mozilla.vrbrowser.ui.callbacks.LibraryContextMenuCallback; import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; +import org.mozilla.vrbrowser.ui.widgets.menus.MenuWidget; import org.mozilla.vrbrowser.utils.AnimationHelper; import java.util.ArrayList; import java.util.Optional; -public class LibraryMenuWidget extends MenuWidget { - - public enum LibraryItemType { - BOOKMARKS, - HISTORY - } +public abstract class LibraryContextMenuWidget extends MenuWidget { public static class LibraryContextMenuItem { private String url; private String title; - private LibraryItemType type; - public LibraryContextMenuItem(@NonNull String url, String title, LibraryItemType type) { + public LibraryContextMenuItem(@NonNull String url, String title) { this.url = url; this.title = title; - this.type = type; } public String getUrl() { @@ -40,18 +34,14 @@ public String getTitle() { return title; } - public LibraryItemType getType() { - return type; - } - } ArrayList mItems; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - Optional mItemDelegate; + Optional mItemDelegate; LibraryContextMenuItem mItem; - public LibraryMenuWidget(Context aContext, LibraryContextMenuItem item, boolean canOpenWindows, boolean isBookmarked) { + public LibraryContextMenuWidget(Context aContext, LibraryContextMenuItem item, boolean canOpenWindows, boolean isBookmarked) { super(aContext, R.layout.library_menu); initialize(); @@ -91,7 +81,7 @@ public void show(int aShowFlags) { @Override public void hide(int aHideFlags) { - AnimationHelper.scaleOut(findViewById(R.id.menuContainer), 100, 0, () -> LibraryMenuWidget.super.hide(aHideFlags)); + AnimationHelper.scaleOut(findViewById(R.id.menuContainer), 100, 0, () -> LibraryContextMenuWidget.super.hide(aHideFlags)); } @Override @@ -107,7 +97,7 @@ protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.context_menu_z_distance); } - public void setItemDelegate(LibraryItemContextMenuClickCallback delegate) { + public void setItemDelegate(LibraryContextMenuCallback delegate) { mItemDelegate = Optional.ofNullable(delegate);; } @@ -126,19 +116,7 @@ private void createMenuItems(boolean canOpenWindows, boolean isBookmarked) { R.drawable.ic_icon_newtab, () -> mItemDelegate.ifPresent((present -> mItemDelegate.get().onOpenInNewTabClick(mItem))))); - if (mItem.type == LibraryItemType.HISTORY) { - mItems.add(new MenuItem(getContext().getString( - isBookmarked ? R.string.history_context_remove_bookmarks : R.string.history_context_add_bookmarks), - isBookmarked ? R.drawable.ic_icon_bookmarked_active : R.drawable.ic_icon_bookmarked, - () -> mItemDelegate.ifPresent((present -> { - if (isBookmarked) { - mItemDelegate.get().onRemoveFromBookmarks(mItem); - - } else { - mItemDelegate.get().onAddToBookmarks(mItem); - } - })))); - } + setupCustomMenuItems(canOpenWindows, isBookmarked); super.updateMenuItems(mItems); @@ -146,4 +124,6 @@ private void createMenuItems(boolean canOpenWindows, boolean isBookmarked) { mWidgetPlacement.height += mBorderWidth * 2; } + protected void setupCustomMenuItems(boolean canOpenWindows, boolean isBookmarked) {} + } diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/SortingContextMenuWidget.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/SortingContextMenuWidget.java new file mode 100644 index 000000000..51ce31cae --- /dev/null +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/menus/library/SortingContextMenuWidget.java @@ -0,0 +1,143 @@ +package org.mozilla.vrbrowser.ui.widgets.menus.library; + +import android.content.Context; +import android.content.res.Configuration; + +import androidx.annotation.IntDef; + +import org.mozilla.vrbrowser.R; +import org.mozilla.vrbrowser.browser.SettingsStore; +import org.mozilla.vrbrowser.ui.widgets.WidgetPlacement; +import org.mozilla.vrbrowser.ui.widgets.menus.MenuWidget; +import org.mozilla.vrbrowser.utils.AnimationHelper; + +import java.util.ArrayList; + +public class SortingContextMenuWidget extends MenuWidget { + + @IntDef(value = {SORT_FILENAME_AZ, SORT_FILENAME_ZA, SORT_DATE_ASC, SORT_DATE_DESC}) + public @interface Order {} + public static final int SORT_FILENAME_AZ = 0; + public static final int SORT_FILENAME_ZA = 1; + public static final int SORT_DATE_ASC = 2; + public static final int SORT_DATE_DESC = 3; + public static final int SORT_SIZE_ASC = 4; + public static final int SORT_SIZE_DESC = 5; + + public interface SortingContextDelegate { + void onItemSelected(@Order int item); + } + + ArrayList mItems; + SortingContextDelegate mItemDelegate; + + public SortingContextMenuWidget(Context aContext) { + super(aContext, R.layout.library_menu); + initialize(); + + createMenuItems(); + } + + private void initialize() { + updateUI(); + } + + @Override + public void updateUI() { + super.updateUI(); + + mAdapter.updateBackgrounds( + R.drawable.library_context_menu_item_background_top, + R.drawable.library_context_menu_item_background_bottom, + R.drawable.library_context_menu_item_background_middle, + R.drawable.library_context_menu_item_background_single); + mAdapter.updateLayoutId(R.layout.library_menu_item); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + updateUI(); + } + + @Override + public void show(int aShowFlags) { + super.show(aShowFlags); + + AnimationHelper.scaleIn(findViewById(R.id.menuContainer), 100, 0, null); + } + + @Override + public void hide(int aHideFlags) { + AnimationHelper.scaleOut(findViewById(R.id.menuContainer), 100, 0, () -> SortingContextMenuWidget.super.hide(aHideFlags)); + } + + @Override + protected void initializeWidgetPlacement(WidgetPlacement aPlacement) { + aPlacement.visible = false; + aPlacement.width = WidgetPlacement.dpDimension(getContext(), R.dimen.context_menu_sorting_width); + aPlacement.parentAnchorX = 0.0f; + aPlacement.parentAnchorY = 1.0f; + aPlacement.anchorX = 1.0f; + aPlacement.anchorY = 1.0f; + aPlacement.opaque = false; + aPlacement.cylinder = true; + aPlacement.translationZ = WidgetPlacement.unitFromMeters(getContext(), R.dimen.context_menu_z_distance); + } + + public void setItemDelegate(SortingContextDelegate delegate) { + mItemDelegate = delegate;; + } + + private void createMenuItems() { + mItems = new ArrayList<>(); + + @Order int order = SettingsStore.getInstance(getContext()).getDownloadsSortingOrder(); + mItems.add(new MenuItem( + getResources().getString(R.string.downloads_sort_download_date_asc), + -1, + () -> { + SettingsStore.getInstance(getContext()).setDownloadsSortingOrder(SORT_DATE_ASC); + if (mItemDelegate != null) { + mItemDelegate.onItemSelected(SORT_DATE_ASC); + } + })); + + mItems.add(new MenuItem( + getResources().getString(R.string.downloads_sort_download_date_desc), + -1, + () -> { + SettingsStore.getInstance(getContext()).setDownloadsSortingOrder(SORT_DATE_DESC); + if (mItemDelegate != null) { + mItemDelegate.onItemSelected(SORT_DATE_DESC); + } + })); + + mItems.add(new MenuItem( + getResources().getString(R.string.downloads_sort_download_size_asc), + -1, + () -> { + SettingsStore.getInstance(getContext()).setDownloadsSortingOrder(SORT_SIZE_ASC); + if (mItemDelegate != null) { + mItemDelegate.onItemSelected(SORT_SIZE_ASC); + } + })); + + mItems.add(new MenuItem( + getResources().getString(R.string.downloads_sort_download_size_desc), + -1, + () -> { + SettingsStore.getInstance(getContext()).setDownloadsSortingOrder(SORT_SIZE_DESC); + if (mItemDelegate != null) { + mItemDelegate.onItemSelected(SORT_SIZE_DESC); + } + })); + + super.updateMenuItems(mItems); + + mWidgetPlacement.height = mItems.size() * WidgetPlacement.dpDimension(getContext(), R.dimen.library_menu_item_height); + mWidgetPlacement.height += mBorderWidth * 2; + } + +} diff --git a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/PrivacyOptionsView.java b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/PrivacyOptionsView.java index e885915ff..e3f32a3c8 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/PrivacyOptionsView.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/ui/widgets/settings/PrivacyOptionsView.java @@ -145,6 +145,10 @@ protected void updateUI() { int etpLevel = SettingsStore.getInstance(getContext()).getTrackingProtectionLevel(); mBinding.trackingProtectionRadio.setOnCheckedChangeListener(mTrackingProtectionListener); setTrackingProtection(mBinding.trackingProtectionRadio.getIdForValue(etpLevel), false); + + @SettingsStore.Storage int downloadsStorage = SettingsStore.getInstance(getContext()).getDownloadsStorage(); + mBinding.downloadsStorage.setOnCheckedChangeListener(mDownloadsStorageListener); + setDownloadsStorage(mBinding.downloadsStorage.getIdForValue(downloadsStorage), false); } private void togglePermission(SwitchSetting aButton, String aPermission) { @@ -202,6 +206,10 @@ public void reject() { setWebXR(value, doApply); }; + private RadioGroupSetting.OnCheckedChangeListener mDownloadsStorageListener = (radioGroup, checkedId, doApply) -> { + setDownloadsStorage(checkedId, true); + }; + private void resetOptions() { if (mBinding.drmContentPlaybackSwitch.isChecked() != SettingsStore.DRM_PLAYBACK_DEFAULT) { setDrmContent(SettingsStore.DRM_PLAYBACK_DEFAULT, true); @@ -238,6 +246,10 @@ private void resetOptions() { if (mBinding.webxrSwitch.isChecked() != SettingsStore.WEBXR_ENABLED_DEFAULT) { setWebXR(SettingsStore.WEBXR_ENABLED_DEFAULT, true); } + + if (!mBinding.downloadsStorage.getValueForId(mBinding.downloadsStorage.getCheckedRadioButtonId()).equals(SettingsStore.DOWNLOADS_STORAGE_DEFAULT)) { + setDownloadsStorage(mBinding.downloadsStorage.getIdForValue(SettingsStore.DOWNLOADS_STORAGE_DEFAULT), true); + } } private void setDrmContent(boolean value, boolean doApply) { @@ -333,6 +345,14 @@ private void setWebXR(boolean value, boolean doApply) { } } + private void setDownloadsStorage(int checkId, boolean doApply) { + mBinding.downloadsStorage.setOnCheckedChangeListener(null); + mBinding.downloadsStorage.setChecked(checkId, doApply); + mBinding.downloadsStorage.setOnCheckedChangeListener(mDownloadsStorageListener); + + SettingsStore.getInstance(getContext()).setDownloadsStorage((Integer)mBinding.downloadsStorage.getValueForId(checkId)); + } + @Override public Point getDimensions() { return new Point( WidgetPlacement.dpDimension(getContext(), R.dimen.settings_dialog_width), diff --git a/app/src/common/shared/org/mozilla/vrbrowser/utils/UrlUtils.java b/app/src/common/shared/org/mozilla/vrbrowser/utils/UrlUtils.java index 3b77d929d..627bc522b 100644 --- a/app/src/common/shared/org/mozilla/vrbrowser/utils/UrlUtils.java +++ b/app/src/common/shared/org/mozilla/vrbrowser/utils/UrlUtils.java @@ -110,8 +110,12 @@ public static Boolean isDataUri(@Nullable String aUri) { return aUri != null && aUri.startsWith("data"); } + public static Boolean isFileUri(@Nullable String aUri) { + return aUri != null && aUri.startsWith("file"); + } + public static Boolean isBlankUri(@Nullable Context context, @Nullable String aUri) { - return aUri != null && context != null && aUri.equals(context.getString(R.string.about_blank)); + return context != null && aUri != null && aUri.equals(context.getString(R.string.about_blank)); } public static String titleBarUrl(@Nullable String aUri) { @@ -149,6 +153,16 @@ public static boolean isBookmarksUrl(@Nullable String url) { return url != null && url.equalsIgnoreCase(ABOUT_BOOKMARKS); } + public static final String ABOUT_DOWNLOADS = "about://downloads"; + + public static boolean isDownloadsUrl(@Nullable String url) { + if (url == null) { + return false; + } + + return url.equalsIgnoreCase(ABOUT_DOWNLOADS); + } + public static final String ABOUT_PRIVATE = "about://privatebrowsing"; public static boolean isPrivateUrl(@Nullable String url) { @@ -156,7 +170,7 @@ public static boolean isPrivateUrl(@Nullable String url) { } public static boolean isAboutPage(@Nullable String url) { - return isHistoryUrl(url) || isBookmarksUrl(url) || isPrivateUrl(url); + return isHistoryUrl(url) || isBookmarksUrl(url) || isDownloadsUrl(url) || isPrivateUrl(url); } public static boolean isContentFeed(Context aContext, @Nullable String url) { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a67475b86..5b8f6bbd0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,5 +42,14 @@ android:process=":crash" android:permission="android.permission.BIND_JOB_SERVICE"> + + + diff --git a/app/src/main/res/color/sorting_panel_content_color.xml b/app/src/main/res/color/sorting_panel_content_color.xml new file mode 100644 index 000000000..34a5813a7 --- /dev/null +++ b/app/src/main/res/color/sorting_panel_content_color.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/downloads_badge.xml b/app/src/main/res/drawable/downloads_badge.xml new file mode 100644 index 000000000..1c601e0cc --- /dev/null +++ b/app/src/main/res/drawable/downloads_badge.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_icon_downloads.xml b/app/src/main/res/drawable/ic_icon_downloads.xml new file mode 100644 index 000000000..09f745e57 --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_downloads.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_icon_downloads_clip.xml b/app/src/main/res/drawable/ic_icon_downloads_clip.xml new file mode 100644 index 000000000..ee2aeadec --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_downloads_clip.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_icon_dropdown.xml b/app/src/main/res/drawable/ic_icon_dropdown.xml new file mode 100644 index 000000000..6c6241dcf --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_dropdown.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_icon_library_clearfromlist.xml b/app/src/main/res/drawable/ic_icon_library_clearfromlist.xml new file mode 100644 index 000000000..f641fc15b --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_library_clearfromlist.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/sorting_button_background.xml b/app/src/main/res/drawable/sorting_button_background.xml new file mode 100644 index 000000000..ab7258dac --- /dev/null +++ b/app/src/main/res/drawable/sorting_button_background.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/bookmark_item.xml b/app/src/main/res/layout/bookmark_item.xml index 506f217d7..2011fc91c 100644 --- a/app/src/main/res/layout/bookmark_item.xml +++ b/app/src/main/res/layout/bookmark_item.xml @@ -59,7 +59,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" - android:ellipsize="end" + android:ellipsize="middle" android:paddingEnd="20dp" android:scrollHorizontally="true" android:singleLine="true" @@ -74,7 +74,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" - android:ellipsize="end" + android:ellipsize="middle" android:paddingEnd="20dp" android:scrollHorizontally="true" android:singleLine="true" diff --git a/app/src/main/res/layout/bookmarks.xml b/app/src/main/res/layout/bookmarks.xml index 51f77ed8b..802e4cde1 100644 --- a/app/src/main/res/layout/bookmarks.xml +++ b/app/src/main/res/layout/bookmarks.xml @@ -86,6 +86,7 @@ android:layout_width="@{bookmarksViewModel.isNarrow ? @dimen/library_icon_size_narrow : @dimen/library_icon_size_wide, default=wrap_content}" android:layout_height="@{bookmarksViewModel.isNarrow ? @dimen/library_icon_size_narrow : @dimen/library_icon_size_wide, default=wrap_content}" android:src="@drawable/ic_icon_bookmarks" + android:tint="@color/concrete" app:srcCompat="@drawable/ic_icon_bookmarks" /> diff --git a/app/src/main/res/layout/download_item.xml b/app/src/main/res/layout/download_item.xml new file mode 100644 index 000000000..10ea5005d --- /dev/null +++ b/app/src/main/res/layout/download_item.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/downloads.xml b/app/src/main/res/layout/downloads.xml new file mode 100644 index 000000000..91c91c978 --- /dev/null +++ b/app/src/main/res/layout/downloads.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/downloads_narrow.xml b/app/src/main/res/layout/downloads_narrow.xml new file mode 100644 index 000000000..f54665659 --- /dev/null +++ b/app/src/main/res/layout/downloads_narrow.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + +