package com.marverenic.music.fragments; import android.annotation.SuppressLint; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.support.v7.app.AlertDialog; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.PopupMenu; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.marverenic.adapter.EnhancedViewHolder; import com.marverenic.adapter.HeterogeneousAdapter; import com.marverenic.music.BuildConfig; import com.marverenic.music.JockeyApplication; import com.marverenic.music.R; import com.marverenic.music.adapter.BasicEmptyState; import com.marverenic.music.data.store.MusicStore; import com.marverenic.music.data.store.PreferenceStore; import com.marverenic.music.dialog.DirectoryDialogFragment; import com.marverenic.music.view.DividerDecoration; import com.tbruyelle.rxpermissions.RxPermissions; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import javax.inject.Inject; import rx.Observable; import timber.log.Timber; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; public class DirectoryListFragment extends Fragment implements View.OnClickListener, DirectoryDialogFragment.OnDirectoryPickListener { private static final String KEY_EXCLUDE_FLAG = "DirectoryListFragment.exclude"; private static final String TAG_DIR_DIALOG = "DirectoryListFragment_DirectoryDialog"; @Inject MusicStore mMusicStore; @Inject PreferenceStore mPreferenceStore; private boolean mExclude; private List<String> mDirectories; private Set<String> mOppositeDirectories; private HeterogeneousAdapter mAdapter; public static DirectoryListFragment newInstance(boolean exclude) { DirectoryListFragment fragment = new DirectoryListFragment(); Bundle args = new Bundle(); args.putBoolean(KEY_EXCLUDE_FLAG, exclude); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mExclude = getArguments().getBoolean(KEY_EXCLUDE_FLAG); JockeyApplication.getComponent(this).inject(this); if (mExclude) { mDirectories = new ArrayList<>(mPreferenceStore.getExcludedDirectories()); mOppositeDirectories = mPreferenceStore.getIncludedDirectories(); } else { mDirectories = new ArrayList<>(mPreferenceStore.getIncludedDirectories()); mOppositeDirectories = mPreferenceStore.getExcludedDirectories(); } Collections.sort(mDirectories); Fragment directoryPicker = getFragmentManager().findFragmentByTag(TAG_DIR_DIALOG); if (directoryPicker instanceof DirectoryDialogFragment) { ((DirectoryDialogFragment) directoryPicker).setDirectoryPickListener(this); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ViewGroup view = (ViewGroup) inflater.inflate(R.layout.fragment_directory_list, container, false); view.findViewById(R.id.fab).setOnClickListener(this); mAdapter = new HeterogeneousAdapter(); mAdapter.setEmptyState(new BasicEmptyState() { @Override public String getMessage() { if (mExclude) { return getString(R.string.empty_excluded_dirs); } else { return getString(R.string.empty_included_dirs); } } @Override public String getDetail() { if (mExclude) { return getString(R.string.empty_excluded_dirs_detail); } else { return getString(R.string.empty_included_dirs_detail); } } }); mAdapter.addSection(new DirectorySection(mDirectories)); RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.list); recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setAdapter(mAdapter); recyclerView.addItemDecoration(new DividerDecoration(getContext(), R.id.subheader_frame)); return view; } @Override public void onResume() { super.onResume(); Toolbar toolbar = (Toolbar) getActivity().findViewById(R.id.toolbar); if (toolbar != null) { toolbar.setTitle((mExclude) ? R.string.pref_directory_exclude : R.string.pref_directory_include); } } @Override public void onPause() { super.onPause(); Set<String> previouslyIncluded = mPreferenceStore.getIncludedDirectories(); Set<String> previouslyExcluded = mPreferenceStore.getExcludedDirectories(); if (mExclude) { mPreferenceStore.setExcludedDirectories(mDirectories); mPreferenceStore.setIncludedDirectories(mOppositeDirectories); } else { mPreferenceStore.setIncludedDirectories(mDirectories); mPreferenceStore.setExcludedDirectories(mOppositeDirectories); } Set<String> currentlyIncluded = mPreferenceStore.getIncludedDirectories(); Set<String> currentlyExcluded = mPreferenceStore.getExcludedDirectories(); boolean isDifferent = !currentlyExcluded.equals(previouslyExcluded) || !currentlyIncluded.equals(previouslyIncluded); if (isDifferent) { mMusicStore.refresh().subscribe( updated -> {}, throwable -> Timber.e(throwable, "Failed to refresh library")); } } private Observable<Boolean> getPermissionObservable() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return Observable.just(true); } return RxPermissions.getInstance(getContext()) .request(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE); } @Override public void onClick(View v) { getPermissionObservable().subscribe( granted -> { if (granted) { showDirectoryPicker(); } else { showInsufficientPermissionSnackbar(v); } }, throwable -> { Timber.e(throwable, "Failed to request storage permission"); }); } private void showDirectoryPicker() { new DirectoryDialogFragment() .setDirectoryPickListener(this) .show(getFragmentManager(), TAG_DIR_DIALOG); } private void showInsufficientPermissionSnackbar(View container) { Snackbar.make(container, R.string.error_no_permission_dirs, Snackbar.LENGTH_LONG) .setAction(R.string.action_open_settings, view -> { Intent intent = new Intent(); Uri uri = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(uri); startActivity(intent); }) .show(); } @Override public void onDirectoryChosen(final File directory) { if (mOppositeDirectories.contains(directory.getAbsolutePath())) { AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); AlertDialog.OnClickListener clickListener = (dialog, which) -> { if (which == DialogInterface.BUTTON_POSITIVE) { mOppositeDirectories.remove(directory.getAbsolutePath()); mDirectories.add(directory.getAbsolutePath()); if (mDirectories.size() == 1) { mAdapter.notifyDataSetChanged(); } else { mAdapter.notifyItemInserted(mDirectories.size() - 1); } } }; String message; String positiveAction; if (mExclude) { message = getString(R.string.confirm_dir_exclude_included, directory.getName()); positiveAction = getString(R.string.action_exclude); } else { message = getString(R.string.confirm_dir_include_excluded, directory.getName()); positiveAction = getString(R.string.action_include); } builder .setMessage(message) .setPositiveButton(positiveAction, clickListener) .setNegativeButton(R.string.action_cancel, null) .show(); } else if (mDirectories.contains(directory.getAbsolutePath())) { String message = getString( (mExclude) ? R.string.confirm_dir_already_excluded : R.string.confirm_dir_already_included, directory.getName()); //noinspection ConstantConditions Snackbar.make(getView(), message, Snackbar.LENGTH_SHORT).show(); } else { mDirectories.add(directory.getAbsolutePath()); if (mDirectories.size() == 1) { mAdapter.notifyDataSetChanged(); } else { mAdapter.notifyItemInserted(mDirectories.size() - 1); } } } private class DirectorySection extends HeterogeneousAdapter.ListSection<String> { public DirectorySection(List<String> data) { super(data); } @Override public EnhancedViewHolder<String> createViewHolder(HeterogeneousAdapter adapter, ViewGroup parent) { return new DirectoryViewHolder( LayoutInflater.from(parent.getContext()) .inflate(R.layout.instance_directory, parent, false)); } } private class DirectoryViewHolder extends EnhancedViewHolder<String> implements View.OnClickListener, PopupMenu.OnMenuItemClickListener { TextView directoryName; TextView directoryPath; String reference; int index; /** * @param itemView The view that this ViewHolder will manage */ public DirectoryViewHolder(View itemView) { super(itemView); directoryName = (TextView) itemView.findViewById(R.id.directory_name); directoryPath = (TextView) itemView.findViewById(R.id.directory_detail); itemView.findViewById(R.id.directory_menu).setOnClickListener(this); } @SuppressLint("SetTextI18n") @Override public void onUpdate(String item, int position) { reference = item; index = position; directoryPath.setText(item + File.separatorChar); int lastSeparatorIndex = item.lastIndexOf(File.separatorChar); if (lastSeparatorIndex < 0) { directoryName.setText(item); } else { directoryName.setText(item.substring(lastSeparatorIndex + 1)); } } @Override public void onClick(View v) { PopupMenu menu = new PopupMenu(itemView.getContext(), v, Gravity.END); menu.getMenu().add(getString(R.string.action_remove)); menu.setOnMenuItemClickListener(this); menu.show(); } @Override public boolean onMenuItemClick(MenuItem item) { final String removedReference = reference; final int removedIndex = index; // Remove this view holder's reference mDirectories.remove(reference); if (mDirectories.isEmpty()) { mAdapter.notifyItemChanged(0); } else { mAdapter.notifyItemRemoved(index); } // Prompt a confirmation Snackbar with undo button Snackbar .make(itemView, getString(R.string.message_removed_directory, reference), Snackbar.LENGTH_LONG) .setAction(R.string.action_undo, v -> { mDirectories.add(removedIndex, removedReference); if (mDirectories.size() == 1) { mAdapter.notifyItemChanged(0); } else { mAdapter.notifyItemRemoved(removedIndex); } }) .show(); return true; } } }