// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.download.settings;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.chromium.chrome.browser.download.DirectoryOption;
import org.chromium.chrome.browser.download.DownloadDirectoryProvider;
import org.chromium.chrome.browser.download.R;
import org.chromium.chrome.browser.download.StringUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Custom adapter that populates the list of which directories the user can choose as their default
* download location.
*/
public class DownloadDirectoryAdapter extends ArrayAdapter<Object> {
/** Delegate to handle directory options results and observe data changes. */
public interface Delegate {
/**
* Called when available download directories are changed, like SD card removal. App level
* UI logic should update to match the new backend data.
*/
void onDirectoryOptionsUpdated();
/** Called after the user selected another download directory option. */
void onDirectorySelectionChanged();
/** Get the helper to access and update the default download directory. */
DownloadLocationHelper getDownloadLocationHelper();
}
/** Allows accessing and updating the default download directory information. */
public interface DownloadLocationHelper {
/** Get the current default download directory. */
String getDownloadDefaultDirectory();
/** Update the default download directory. */
void setDownloadAndSaveFileDefaultDirectory(String directory);
}
public static int NO_SELECTED_ITEM_ID = -1;
public static int SELECTED_ITEM_NOT_INITIALIZED = -2;
protected int mSelectedPosition = SELECTED_ITEM_NOT_INITIALIZED;
private Context mContext;
private LayoutInflater mLayoutInflater;
protected Delegate mDelegate;
private List<DirectoryOption> mCanonicalOptions = new ArrayList<>();
private List<DirectoryOption> mAdditionalOptions = new ArrayList<>();
private List<DirectoryOption> mErrorOptions = new ArrayList<>();
public DownloadDirectoryAdapter(@NonNull Context context, @NonNull Delegate delegate) {
super(context, android.R.layout.simple_spinner_item);
mContext = context;
mDelegate = delegate;
mLayoutInflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return mCanonicalOptions.size() + mAdditionalOptions.size() + mErrorOptions.size();
}
@Nullable
@Override
public Object getItem(int position) {
if (!mErrorOptions.isEmpty()) {
assert position == 0;
assert getCount() == 1;
return mErrorOptions.get(position);
}
return position < mCanonicalOptions.size()
? mCanonicalOptions.get(position)
: mAdditionalOptions.get(position - mCanonicalOptions.size());
}
@Override
public long getItemId(int position) {
return position;
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view =
convertView != null
? convertView
: mLayoutInflater.inflate(R.layout.download_location_spinner_item, null);
view.setTag(position);
DirectoryOption directoryOption = (DirectoryOption) getItem(position);
if (directoryOption == null) return view;
TextView titleText = (TextView) view.findViewById(R.id.title);
titleText.setText(directoryOption.name);
// ModalDialogView may do a measure pass on the view hierarchy to limit the layout inside
// certain area, where LayoutParams cannot be null.
if (view.getLayoutParams() == null) {
view.setLayoutParams(
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
}
return view;
}
@Override
public View getDropDownView(
int position, @Nullable View convertView, @NonNull ViewGroup parent) {
View view =
convertView != null
? convertView
: mLayoutInflater.inflate(
R.layout.download_location_spinner_dropdown_item, null);
view.setTag(position);
DirectoryOption directoryOption = (DirectoryOption) getItem(position);
if (directoryOption == null) return view;
TextView titleText = (TextView) view.findViewById(R.id.title);
TextView summaryText = (TextView) view.findViewById(R.id.description);
boolean enabled = isEnabled(position);
titleText.setText(directoryOption.name);
titleText.setEnabled(enabled);
summaryText.setEnabled(enabled);
if (enabled) {
summaryText.setText(
StringUtils.getAvailableBytesForUi(mContext, directoryOption.availableSpace));
} else {
if (mErrorOptions.isEmpty()) {
summaryText.setText(mContext.getText(R.string.download_location_not_enough_space));
} else {
summaryText.setVisibility(View.GONE);
}
}
ImageView imageView = view.findViewById(R.id.start_icon);
imageView.setVisibility(View.GONE);
return view;
}
@Override
public boolean isEnabled(int position) {
DirectoryOption directoryOption = (DirectoryOption) getItem(position);
return directoryOption != null && directoryOption.availableSpace != 0;
}
/**
* @return ID of the directory option that matches the default download location.
*/
public int getSelectedItemId() {
return mSelectedPosition;
}
private void initSelectedIdFromPref() {
if (!mErrorOptions.isEmpty()) return;
int selectedId = NO_SELECTED_ITEM_ID;
String defaultLocation =
mDelegate.getDownloadLocationHelper().getDownloadDefaultDirectory();
for (int i = 0; i < getCount(); i++) {
DirectoryOption option = (DirectoryOption) getItem(i);
if (option == null) continue;
if (defaultLocation.equals(option.location)) {
selectedId = i;
break;
}
}
mSelectedPosition = selectedId;
}
/**
* In the case that there is no selected item ID/the selected item ID is invalid (ie. there is
* not enough space), select either the default or the next valid item ID. Set the default to be
* this item and return the ID.
*
* @return ID of the first valid, selectable item and the new default location.
*/
public int useFirstValidSelectableItemId() {
for (int i = 0; i < getCount(); i++) {
DirectoryOption option = (DirectoryOption) getItem(i);
if (option == null) continue;
if (option.availableSpace > 0) {
mDelegate
.getDownloadLocationHelper()
.setDownloadAndSaveFileDefaultDirectory(option.location);
mSelectedPosition = i;
return i;
}
}
// Display an option that says there are no available download locations.
adjustErrorDirectoryOption();
return 0;
}
/**
* Get the ID of the suggested item based on total bytes and threshold.
* @param totalBytes The total bytes of the download file.
* @return ID of the suggested item and the new default location.
*/
public int useSuggestedItemId(long totalBytes) {
double maxSpaceLeft = 0;
int suggestedId = NO_SELECTED_ITEM_ID;
String defaultLocation =
mDelegate.getDownloadLocationHelper().getDownloadDefaultDirectory();
for (int i = 0; i < getCount(); i++) {
DirectoryOption option = (DirectoryOption) getItem(i);
if (option == null) continue;
if (defaultLocation.equals(option.location)) continue;
double spaceLeft = (double) (option.availableSpace - totalBytes) / option.totalSpace;
// If a larger storage is found, mark it as the suggested option.
if (spaceLeft > maxSpaceLeft) {
maxSpaceLeft = spaceLeft;
suggestedId = i;
}
}
// If there is a suggested option, set it as default directory and return its position.
if (suggestedId != NO_SELECTED_ITEM_ID) {
DirectoryOption suggestedOption = (DirectoryOption) getItem(suggestedId);
mSelectedPosition = suggestedId;
return suggestedId;
}
// Display an option that says there are no available download locations.
adjustErrorDirectoryOption();
return 0;
}
boolean hasAvailableLocations() {
return mErrorOptions.isEmpty();
}
/** Update the list of items. */
public void update() {
mCanonicalOptions.clear();
mAdditionalOptions.clear();
mErrorOptions.clear();
// Retrieve all download directories.
DownloadDirectoryProvider.getInstance()
.getAllDirectoriesOptions(
(ArrayList<DirectoryOption> dirs) -> {
onDirectoryOptionsRetrieved(dirs);
});
}
private void onDirectoryOptionsRetrieved(ArrayList<DirectoryOption> dirs) {
int numOtherAdditionalDirectories = 0;
for (DirectoryOption dir : dirs) {
DirectoryOption directory = (DirectoryOption) dir.clone();
switch (directory.type) {
case DirectoryOption.DownloadLocationDirectoryType.DEFAULT:
directory.name = mContext.getString(R.string.menu_downloads);
mCanonicalOptions.add(directory);
break;
case DirectoryOption.DownloadLocationDirectoryType.ADDITIONAL:
String directoryName =
(numOtherAdditionalDirectories > 0)
? mContext.getString(
R.string.downloads_location_sd_card_number,
numOtherAdditionalDirectories + 1)
: mContext.getString(R.string.downloads_location_sd_card);
directory.name = directoryName;
mAdditionalOptions.add(directory);
numOtherAdditionalDirectories++;
break;
case DirectoryOption.DownloadLocationDirectoryType.ERROR:
directory.name =
mContext.getString(R.string.download_location_no_available_locations);
mErrorOptions.add(directory);
break;
default:
break;
}
}
// Setup the selection.
initSelectedIdFromPref();
// Update lower Android level UI widgets.
notifyDataSetChanged();
// Update higher app level UI logic.
mDelegate.onDirectoryOptionsUpdated();
}
private void adjustErrorDirectoryOption() {
if ((mCanonicalOptions.size() + mAdditionalOptions.size()) > 0) {
mErrorOptions.clear();
} else {
mErrorOptions.add(
new DirectoryOption(
mContext.getString(R.string.download_location_no_available_locations),
null,
0,
0,
DirectoryOption.DownloadLocationDirectoryType.ERROR));
}
}
}