diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java index a75c8561f..cb9ce8947 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java @@ -246,13 +246,6 @@ public abstract class BaseStateFragment extends BaseFragment implements ViewC // Utils //////////////////////////////////////////////////////////////////////////*/ - public void setTitle(String title) { - if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]"); - if (activity != null && activity.getSupportActionBar() != null) { - activity.getSupportActionBar().setTitle(title); - } - } - protected void openUrlInBrowser(String url) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title))); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index abc150e7d..20607b3a0 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -106,7 +106,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte super.onCreateOptionsMenu(menu, inflater); if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]"); inflater.inflate(R.menu.main_fragment_menu, menu); - SubMenu kioskMenu = menu.addSubMenu(getString(R.string.kiosk)); + SubMenu kioskMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, 200, getString(R.string.kiosk)); try { createKioskMenu(kioskMenu, inflater); } catch (Exception e) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 8c9945149..580e16825 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -141,8 +141,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem @Override public void selected(StreamInfoItem selectedItem) { onItemSelected(selectedItem); - NavigationHelper.openVideoDetailFragment( - useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(), + NavigationHelper.openVideoDetailFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); } @@ -156,8 +155,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem @Override public void selected(ChannelInfoItem selectedItem) { onItemSelected(selectedItem); - NavigationHelper.openChannelFragment( - useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(), + NavigationHelper.openChannelFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); } }); @@ -166,8 +164,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem @Override public void selected(PlaylistInfoItem selectedItem) { onItemSelected(selectedItem); - NavigationHelper.openPlaylistFragment( - useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(), + NavigationHelper.openPlaylistFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(), selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName()); } }); @@ -230,7 +227,7 @@ public abstract class BaseListFragment extends BaseStateFragment implem ActionBar supportActionBar = activity.getSupportActionBar(); if (supportActionBar != null) { supportActionBar.setDisplayShowTitleEnabled(true); - if(useAsFrontPage) { + if (useAsFrontPage) { supportActionBar.setDisplayHomeAsUpEnabled(false); } else { supportActionBar.setDisplayHomeAsUpEnabled(true); @@ -277,9 +274,8 @@ public abstract class BaseListFragment extends BaseStateFragment implem @Override public void showListFooter(final boolean show) { - itemsList.post(new Runnable() { - @Override - public void run() { + itemsList.post(() -> { + if (infoListAdapter != null && itemsList != null) { infoListAdapter.showFooter(show); } }); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/subscription/ImportConfirmationDialog.java b/app/src/main/java/org/schabi/newpipe/fragments/subscription/ImportConfirmationDialog.java new file mode 100644 index 000000000..3a64c22c7 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/subscription/ImportConfirmationDialog.java @@ -0,0 +1,64 @@ +package org.schabi.newpipe.fragments.subscription; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Intent; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.util.ThemeHelper; + +import icepick.Icepick; +import icepick.State; + +public class ImportConfirmationDialog extends DialogFragment { + @State + protected Intent resultServiceIntent; + + public void setResultServiceIntent(Intent resultServiceIntent) { + this.resultServiceIntent = resultServiceIntent; + } + + public static void show(@NonNull Fragment fragment, @NonNull Intent resultServiceIntent) { + if (fragment.getFragmentManager() == null) return; + + final ImportConfirmationDialog confirmationDialog = new ImportConfirmationDialog(); + confirmationDialog.setResultServiceIntent(resultServiceIntent); + confirmationDialog.show(fragment.getFragmentManager(), null); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + return new AlertDialog.Builder(getContext(), ThemeHelper.getDialogTheme(getContext())) + .setMessage(R.string.import_network_expensive_warning) + .setCancelable(true) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { + if (resultServiceIntent != null && getContext() != null) { + getContext().startService(resultServiceIntent); + } + dismiss(); + }) + .create(); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (resultServiceIntent == null) throw new IllegalStateException("Result intent is null"); + + Icepick.restoreInstanceState(this, savedInstanceState); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + Icepick.saveInstanceState(this, outState); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java index a91cca908..1e69732b7 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/subscription/SubscriptionFragment.java @@ -1,30 +1,62 @@ package org.schabi.newpipe.fragments.subscription; +import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Color; +import android.graphics.PorterDuff; import android.os.Bundle; +import android.os.Environment; import android.os.Parcelable; +import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.content.LocalBroadcastManager; +import android.support.v7.app.ActionBar; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import com.nononsenseapps.filepicker.Utils; import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.InfoItem; +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.channel.ChannelInfoItem; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor; import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.report.UserAction; +import org.schabi.newpipe.subscription.SubscriptionService; +import org.schabi.newpipe.subscription.services.SubscriptionsExportService; +import org.schabi.newpipe.subscription.services.SubscriptionsImportService; +import org.schabi.newpipe.util.FilePickerActivityHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.OnClickGesture; +import org.schabi.newpipe.util.ServiceHelper; +import org.schabi.newpipe.util.ThemeHelper; +import org.schabi.newpipe.views.CollapsibleView; +import java.io.File; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.List; +import java.util.Locale; import icepick.State; import io.reactivex.Observer; @@ -33,18 +65,29 @@ import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; +import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.KEY_MODE; +import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.KEY_VALUE; +import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE; +import static org.schabi.newpipe.util.AnimationUtils.animateRotation; import static org.schabi.newpipe.util.AnimationUtils.animateView; public class SubscriptionFragment extends BaseStateFragment> { - private View headerRootLayout; + private static final int REQUEST_EXPORT_CODE = 666; + private static final int REQUEST_IMPORT_CODE = 667; - private InfoListAdapter infoListAdapter; private RecyclerView itemsList; - @State protected Parcelable itemsListState; + private InfoListAdapter infoListAdapter; + + private View headerRootLayout; + private View whatsNewItemListHeader; + private View importExportListHeader; + + @State + protected Parcelable importExportOptionsState; + private CollapsibleView importExportOptions; - /* Used for independent events */ private CompositeDisposable disposables = new CompositeDisposable(); private SubscriptionService subscriptionService; @@ -52,39 +95,48 @@ public class SubscriptionFragment extends BaseStateFragment onImportPreviousSelected()); + + final int iconColor = ThemeHelper.isLightThemeSelected(getContext()) ? Color.BLACK : Color.WHITE; + final String[] services = getResources().getStringArray(R.array.service_list); + for (String serviceName : services) { + try { + final StreamingService service = NewPipe.getService(serviceName); + + final SubscriptionExtractor subscriptionExtractor = service.getSubscriptionExtractor(); + if (subscriptionExtractor == null) continue; + + final List supportedSources = subscriptionExtractor.getSupportedSources(); + if (supportedSources.isEmpty()) continue; + + final View itemView = addItemView(serviceName, ServiceHelper.getIcon(service.getServiceId()), listHolder); + final ImageView iconView = itemView.findViewById(android.R.id.icon1); + iconView.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN); + + itemView.setOnClickListener(selectedItem -> onImportFromServiceSelected(service.getServiceId())); + } catch (ExtractionException e) { + throw new RuntimeException("Services array contains an entry that it's not a valid service name (" + serviceName + ")", e); + } + } + } + + private void setupExportToItems(final ViewGroup listHolder) { + final View previousBackupItem = addItemView(getString(R.string.file), ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_save), listHolder); + previousBackupItem.setOnClickListener(item -> onExportSelected()); + } + + private void onImportFromServiceSelected(int serviceId) { + if (getParentFragment() == null) return; + NavigationHelper.openSubscriptionsImportFragment(getParentFragment().getFragmentManager(), serviceId); + } + + private void onImportPreviousSelected() { + startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE); + } + + private void onExportSelected() { + final String date = new SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(new Date()); + final String exportName = "newpipe_subscriptions_" + date + ".json"; + final File exportFile = new File(Environment.getExternalStorageDirectory(), exportName); + + startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.getAbsolutePath()), REQUEST_EXPORT_CODE); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (data != null && data.getData() != null && resultCode == Activity.RESULT_OK) { + if (requestCode == REQUEST_EXPORT_CODE) { + final File exportFile = Utils.getFileForUri(data.getData()); + if (!exportFile.getParentFile().canWrite() || !exportFile.getParentFile().canRead()) { + Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show(); + } else { + activity.startService(new Intent(activity, SubscriptionsExportService.class) + .putExtra(SubscriptionsExportService.KEY_FILE_PATH, exportFile.getAbsolutePath())); + } + } else if (requestCode == REQUEST_IMPORT_CODE) { + final String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); + ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class) + .putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE) + .putExtra(KEY_VALUE, path)); + } + } + } + /*///////////////////////////////////////////////////////////////////////// // Fragment Views - /////////////////////////////////////////////////////////////////////////// + /////////////////////////////////////////////////////////////////////////*/ @Override protected void initViews(View rootView, Bundle savedInstanceState) { @@ -116,9 +290,27 @@ public class SubscriptionFragment extends BaseStateFragment animateRotation(iconView, 250, newState == CollapsibleView.COLLAPSED ? 0 : 180); } @Override @@ -130,12 +322,14 @@ public class SubscriptionFragment extends BaseStateFragment + //noinspection ConstantConditions + whatsNewItemListHeader.setOnClickListener(v -> NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager())); + importExportListHeader.setOnClickListener(v -> importExportOptions.switchState()); } private void resetFragment() { @@ -189,6 +383,7 @@ public class SubscriptionFragment extends BaseStateFragment supportedSources; + private String relatedUrl; + @StringRes + private int instructionsString; + + public static SubscriptionsImportFragment getInstance(int serviceId) { + SubscriptionsImportFragment instance = new SubscriptionsImportFragment(); + instance.setInitialData(serviceId); + return instance; + } + + public void setInitialData(int serviceId) { + this.currentServiceId = serviceId; + } + + /*////////////////////////////////////////////////////////////////////////// + // Views + //////////////////////////////////////////////////////////////////////////*/ + + private TextView infoTextView; + + private EditText inputText; + private Button inputButton; + + /////////////////////////////////////////////////////////////////////////// + // Fragment LifeCycle + /////////////////////////////////////////////////////////////////////////// + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setupServiceVariables(); + if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) { + ErrorActivity.reportError(activity, Collections.emptyList(), null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, + NewPipe.getNameOfService(currentServiceId), "Service don't support importing", R.string.general_error)); + activity.finish(); + } + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + if (isVisibleToUser) { + setTitle(getString(R.string.import_title)); + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_import, container, false); + } + + /*///////////////////////////////////////////////////////////////////////// + // Fragment Views + /////////////////////////////////////////////////////////////////////////*/ + + @Override + protected void initViews(View rootView, Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + inputButton = rootView.findViewById(R.id.input_button); + inputText = rootView.findViewById(R.id.input_text); + + infoTextView = rootView.findViewById(R.id.info_text_view); + + // TODO: Support services that can import from more than one source (show the option to the user) + if (supportedSources.contains(CHANNEL_URL)) { + inputButton.setText(R.string.import_title); + inputText.setVisibility(View.VISIBLE); + inputText.setHint(ServiceHelper.getImportInstructionsHint(currentServiceId)); + } else { + inputButton.setText(R.string.import_file_title); + } + + if (instructionsString != 0) { + if (TextUtils.isEmpty(relatedUrl)) { + setInfoText(getString(instructionsString)); + } else { + setInfoText(getString(instructionsString, relatedUrl)); + } + } else { + setInfoText(""); + } + + ActionBar supportActionBar = activity.getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayShowTitleEnabled(true); + setTitle(getString(R.string.import_title)); + } + } + + @Override + protected void initListeners() { + super.initListeners(); + inputButton.setOnClickListener(v -> onImportClicked()); + } + + private void onImportClicked() { + if (inputText.getVisibility() == View.VISIBLE) { + final String value = inputText.getText().toString(); + if (!value.isEmpty()) onImportUrl(value); + } else { + onImportFile(); + } + } + + public void onImportUrl(String value) { + ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class) + .putExtra(KEY_MODE, CHANNEL_URL_MODE) + .putExtra(KEY_VALUE, value) + .putExtra(Constants.KEY_SERVICE_ID, currentServiceId)); + } + + public void onImportFile() { + startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_FILE_CODE); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (data == null) return; + + if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMPORT_FILE_CODE && data.getData() != null) { + final String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); + ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class) + .putExtra(KEY_MODE, INPUT_STREAM_MODE) + .putExtra(KEY_VALUE, path) + .putExtra(Constants.KEY_SERVICE_ID, currentServiceId)); + } + } + + /////////////////////////////////////////////////////////////////////////// + // Subscriptions + /////////////////////////////////////////////////////////////////////////// + + private void setupServiceVariables() { + if (currentServiceId != Constants.NO_SERVICE_ID) { + try { + final SubscriptionExtractor extractor = NewPipe.getService(currentServiceId).getSubscriptionExtractor(); + supportedSources = extractor.getSupportedSources(); + relatedUrl = extractor.getRelatedUrl(); + instructionsString = ServiceHelper.getImportInstructions(currentServiceId); + return; + } catch (ExtractionException ignored) { + } + } + + supportedSources = Collections.emptyList(); + relatedUrl = null; + instructionsString = 0; + } + + private void setInfoText(String infoString) { + infoTextView.setText(infoString); + LinkifyCompat.addLinks(infoTextView, Linkify.WEB_URLS); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java index c954211fa..3c5f16929 100644 --- a/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/AnimationUtils.java @@ -1,3 +1,22 @@ +/* + * Copyright 2018 Mauricio Colli + * AnimationUtils.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.schabi.newpipe.util; import android.animation.Animator; @@ -19,7 +38,9 @@ public class AnimationUtils { private static final boolean DEBUG = MainActivity.DEBUG; public enum Type { - ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA + ALPHA, + SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, + SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA } public static void animateView(View view, boolean enterOrExit, long duration) { @@ -168,6 +189,58 @@ public class AnimationUtils { viewPropertyAnimator.start(); } + public static ValueAnimator animateHeight(final View view, long duration, int targetHeight) { + final int height = view.getHeight(); + if (DEBUG) { + Log.d(TAG, "animateHeight: duration = [" + duration + "], from " + height + " to → " + targetHeight + " in: " + view); + } + + ValueAnimator animator = ValueAnimator.ofFloat(height, targetHeight); + animator.setInterpolator(new FastOutSlowInInterpolator()); + animator.setDuration(duration); + animator.addUpdateListener(animation -> { + final float value = (float) animation.getAnimatedValue(); + view.getLayoutParams().height = (int) value; + view.requestLayout(); + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.getLayoutParams().height = targetHeight; + view.requestLayout(); + } + + @Override + public void onAnimationCancel(Animator animation) { + view.getLayoutParams().height = targetHeight; + view.requestLayout(); + } + }); + animator.start(); + + return animator; + } + + public static void animateRotation(final View view, long duration, int targetRotation) { + if (DEBUG) { + Log.d(TAG, "animateRotation: duration = [" + duration + "], from " + view.getRotation() + " to → " + targetRotation + " in: " + view); + } + view.animate().setListener(null).cancel(); + + view.animate().rotation(targetRotation).setDuration(duration).setInterpolator(new FastOutSlowInInterpolator()) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + view.setRotation(targetRotation); + } + + @Override + public void onAnimationEnd(Animator animation) { + view.setRotation(targetRotation); + } + }).start(); + } + /*////////////////////////////////////////////////////////////////////////// // Internals //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java b/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java index 5f588c5ca..20554ce59 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/FilePickerActivityHelper.java @@ -1,10 +1,32 @@ package org.schabi.newpipe.util; +import android.content.Context; +import android.content.Intent; import android.os.Bundle; +import android.os.Environment; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.Loader; +import android.support.v7.util.SortedList; +import android.support.v7.widget.RecyclerView; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import com.nononsenseapps.filepicker.AbstractFilePickerFragment; +import com.nononsenseapps.filepicker.FilePickerFragment; + import org.schabi.newpipe.R; +import java.io.File; + public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.FilePickerActivity { + private CustomFilePickerFragment currentFragment; + @Override public void onCreate(Bundle savedInstanceState) { if(ThemeHelper.isLightThemeSelected(this)) { @@ -14,4 +36,98 @@ public class FilePickerActivityHelper extends com.nononsenseapps.filepicker.File } super.onCreate(savedInstanceState); } + + @Override + public void onBackPressed() { + // If at top most level, normal behaviour + if (currentFragment.isBackTop()) { + super.onBackPressed(); + } else { + // Else go up + currentFragment.goUp(); + } + } + + @Override + protected AbstractFilePickerFragment getFragment(@Nullable String startPath, int mode, boolean allowMultiple, boolean allowCreateDir, boolean allowExistingFile, boolean singleClick) { + final CustomFilePickerFragment fragment = new CustomFilePickerFragment(); + fragment.setArgs(startPath != null ? startPath : Environment.getExternalStorageDirectory().getPath(), + mode, allowMultiple, allowCreateDir, allowExistingFile, singleClick); + return currentFragment = fragment; + } + + public static Intent chooseSingleFile(@NonNull Context context) { + return new Intent(context, FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false) + .putExtra(FilePickerActivityHelper.EXTRA_SINGLE_CLICK, true) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_FILE); + } + + public static Intent chooseFileToSave(@NonNull Context context, @Nullable String startPath) { + return new Intent(context, FilePickerActivityHelper.class) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true) + .putExtra(FilePickerActivityHelper.EXTRA_ALLOW_EXISTING_FILE, true) + .putExtra(FilePickerActivityHelper.EXTRA_START_PATH, startPath) + .putExtra(FilePickerActivityHelper.EXTRA_MODE, FilePickerActivityHelper.MODE_NEW_FILE); + } + + /*////////////////////////////////////////////////////////////////////////// + // Internal + //////////////////////////////////////////////////////////////////////////*/ + + public static class CustomFilePickerFragment extends FilePickerFragment { + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return super.onCreateView(inflater, container, savedInstanceState); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final RecyclerView.ViewHolder viewHolder = super.onCreateViewHolder(parent, viewType); + + final View view = viewHolder.itemView.findViewById(android.R.id.text1); + if (view instanceof TextView) { + ((TextView) view).setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen.file_picker_items_text_size)); + } + + return viewHolder; + } + + @Override + public void onClickOk(@NonNull View view) { + if (mode == MODE_NEW_FILE && getNewFileName().isEmpty()) { + if (mToast != null) mToast.cancel(); + mToast = Toast.makeText(getActivity(), R.string.file_name_empty_error, Toast.LENGTH_SHORT); + mToast.show(); + return; + } + + super.onClickOk(view); + } + + public File getBackTop() { + if (getArguments() == null) return Environment.getExternalStorageDirectory(); + + final String path = getArguments().getString(KEY_START_PATH, "/"); + if (path.contains(Environment.getExternalStorageDirectory().getPath())) { + return Environment.getExternalStorageDirectory(); + } + + return getPath(path); + } + + public boolean isBackTop() { + return compareFiles(mCurrentPath, getBackTop()) == 0 || compareFiles(mCurrentPath, new File("/")) == 0; + } + + @Override + public void onLoadFinished(Loader> loader, SortedList data) { + super.onLoadFinished(loader, data); + layoutManager.scrollToPosition(0); + } + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index ee94ac81f..26088a64c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.util; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; @@ -11,6 +12,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; import android.support.v7.app.AlertDialog; import android.util.Log; import android.widget.Toast; @@ -38,6 +40,7 @@ import org.schabi.newpipe.fragments.list.search.SearchFragment; import org.schabi.newpipe.fragments.local.bookmark.LastPlayedFragment; import org.schabi.newpipe.fragments.local.bookmark.LocalPlaylistFragment; import org.schabi.newpipe.fragments.local.bookmark.MostPlayedFragment; +import org.schabi.newpipe.fragments.subscription.SubscriptionsImportFragment; import org.schabi.newpipe.history.HistoryActivity; import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayerActivity; @@ -247,6 +250,12 @@ public class NavigationHelper { // Through FragmentManager //////////////////////////////////////////////////////////////////////////*/ + @SuppressLint("CommitTransaction") + private static FragmentTransaction defaultTransaction(FragmentManager fragmentManager) { + return fragmentManager.beginTransaction() + .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out); + } + public static void gotoMainFragment(FragmentManager fragmentManager) { ImageLoader.getInstance().clearMemoryCache(); @@ -258,8 +267,7 @@ public class NavigationHelper { InfoCache.getInstance().trimCache(); fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, new MainFragment()) .addToBackStack(MAIN_FRAGMENT_TAG) .commit(); @@ -276,8 +284,7 @@ public class NavigationHelper { } public static void openSearchFragment(FragmentManager fragmentManager, int serviceId, String query) { - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, SearchFragment.getInstance(serviceId, query)) .addToBackStack(SEARCH_FRAGMENT_TAG) .commit(); @@ -301,8 +308,7 @@ public class NavigationHelper { VideoDetailFragment instance = VideoDetailFragment.getInstance(serviceId, url, title); instance.setAutoplay(autoPlay); - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, instance) .addToBackStack(null) .commit(); @@ -310,8 +316,7 @@ public class NavigationHelper { public static void openChannelFragment(FragmentManager fragmentManager, int serviceId, String url, String name) { if (name == null) name = ""; - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, ChannelFragment.getInstance(serviceId, url, name)) .addToBackStack(null) .commit(); @@ -319,25 +324,21 @@ public class NavigationHelper { public static void openPlaylistFragment(FragmentManager fragmentManager, int serviceId, String url, String name) { if (name == null) name = ""; - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, PlaylistFragment.getInstance(serviceId, url, name)) .addToBackStack(null) .commit(); } public static void openWhatsNewFragment(FragmentManager fragmentManager) { - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, new FeedFragment()) .addToBackStack(null) .commit(); } - public static void openKioskFragment(FragmentManager fragmentManager, int serviceId, String kioskId) - throws ExtractionException { - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + public static void openKioskFragment(FragmentManager fragmentManager, int serviceId, String kioskId) throws ExtractionException { + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, KioskFragment.getInstance(serviceId, kioskId)) .addToBackStack(null) .commit(); @@ -345,28 +346,33 @@ public class NavigationHelper { public static void openLocalPlaylistFragment(FragmentManager fragmentManager, long playlistId, String name) { if (name == null) name = ""; - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, LocalPlaylistFragment.getInstance(playlistId, name)) .addToBackStack(null) .commit(); } public static void openLastPlayedFragment(FragmentManager fragmentManager) { - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, new LastPlayedFragment()) .addToBackStack(null) .commit(); } public static void openMostPlayedFragment(FragmentManager fragmentManager) { - fragmentManager.beginTransaction() - .setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out) + defaultTransaction(fragmentManager) .replace(R.id.fragment_holder, new MostPlayedFragment()) .addToBackStack(null) .commit(); } + + public static void openSubscriptionsImportFragment(FragmentManager fragmentManager, int serviceId) { + defaultTransaction(fragmentManager) + .replace(R.id.fragment_holder, SubscriptionsImportFragment.getInstance(serviceId)) + .addToBackStack(null) + .commit(); + } + /*////////////////////////////////////////////////////////////////////////// // Through Intents //////////////////////////////////////////////////////////////////////////*/ diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index 55c6e68f2..7d71750eb 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -3,6 +3,7 @@ package org.schabi.newpipe.util; import android.content.Context; import android.preference.PreferenceManager; import android.support.annotation.DrawableRes; +import android.support.annotation.StringRes; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; @@ -26,6 +27,39 @@ public class ServiceHelper { } } + /** + * Get a resource string with instructions for importing subscriptions for each service. + * + * @return the string resource containing the instructions or -1 if the service don't support it + */ + @StringRes + public static int getImportInstructions(int serviceId) { + switch (serviceId) { + case 0: + return R.string.import_youtube_instructions; + case 1: + return R.string.import_soundcloud_instructions; + default: + return -1; + } + } + + /** + * For services that support importing from a channel url, return a hint that will + * be used in the EditText that the user will type in his channel url. + * + * @return the hint's string resource or -1 if the service don't support it + */ + @StringRes + public static int getImportInstructionsHint(int serviceId) { + switch (serviceId) { + case 1: + return R.string.import_soundcloud_instructions_hint; + default: + return -1; + } + } + public static int getSelectedServiceId(Context context) { if (BuildConfig.BUILD_TYPE.equals("release")) return DEFAULT_FALLBACK_SERVICE.getServiceId(); diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index 824ac4a9d..1edc4dfec 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -1,3 +1,22 @@ +/* + * Copyright 2018 Mauricio Colli + * ThemeHelper.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.schabi.newpipe.util; import android.content.Context; @@ -5,6 +24,9 @@ import android.content.res.TypedArray; import android.preference.PreferenceManager; import android.support.annotation.AttrRes; import android.support.annotation.StyleRes; +import android.support.v4.content.ContextCompat; +import android.util.TypedValue; +import android.view.ContextThemeWrapper; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.NewPipe; @@ -41,16 +63,57 @@ public class ThemeHelper { * @param context context to get the preference */ public static boolean isLightThemeSelected(Context context) { - return getSelectedTheme(context).equals(context.getResources().getString(R.string.light_theme_key)); + return getSelectedThemeString(context).equals(context.getResources().getString(R.string.light_theme_key)); } + + /** + * Create and return a wrapped context with the default selected theme set. + * + * @param baseContext the base context for the wrapper + * @return a wrapped-styled context + */ + public static Context getThemedContext(Context baseContext) { + return new ContextThemeWrapper(baseContext, getThemeForService(baseContext, -1)); + } + + /** + * Return the selected theme without being styled to any service (see {@link #getThemeForService(Context, int)}). + * + * @param context context to get the selected theme + * @return the selected style (the default one) + */ + @StyleRes + public static int getDefaultTheme(Context context) { + return getThemeForService(context, -1); + } + + /** + * Return a dialog theme styled according to the (default) selected theme. + * + * @param context context to get the selected theme + * @return the dialog style (the default one) + */ + @StyleRes + public static int getDialogTheme(Context context) { + return isLightThemeSelected(context) ? R.style.LightDialogTheme : R.style.DarkDialogTheme; + } + + /** + * Return the selected theme styled according to the serviceId. + * + * @param context context to get the selected theme + * @param serviceId return a theme styled to this service, + * -1 to get the default + * @return the selected style (styled) + */ @StyleRes public static int getThemeForService(Context context, int serviceId) { String lightTheme = context.getResources().getString(R.string.light_theme_key); String darkTheme = context.getResources().getString(R.string.dark_theme_key); String blackTheme = context.getResources().getString(R.string.black_theme_key); - String selectedTheme = getSelectedTheme(context); + String selectedTheme = getSelectedThemeString(context); int defaultTheme = R.style.DarkTheme; if (selectedTheme.equals(lightTheme)) defaultTheme = R.style.LightTheme; @@ -83,19 +146,13 @@ public class ThemeHelper { return defaultTheme; } - public static String getSelectedTheme(Context context) { - String themeKey = context.getString(R.string.theme_key); - String defaultTheme = context.getResources().getString(R.string.default_theme_value); - return PreferenceManager.getDefaultSharedPreferences(context).getString(themeKey, defaultTheme); - } - @StyleRes public static int getSettingsThemeStyle(Context context) { String lightTheme = context.getResources().getString(R.string.light_theme_key); String darkTheme = context.getResources().getString(R.string.dark_theme_key); String blackTheme = context.getResources().getString(R.string.black_theme_key); - String selectedTheme = getSelectedTheme(context); + String selectedTheme = getSelectedThemeString(context); if (selectedTheme.equals(lightTheme)) return R.style.LightSettingsTheme; else if (selectedTheme.equals(blackTheme)) return R.style.BlackSettingsTheme; @@ -113,4 +170,24 @@ public class ThemeHelper { a.recycle(); return attributeResourceId; } + + /** + * Get a color from an attr styled according to the the context's theme. + */ + public static int resolveColorFromAttr(Context context, @AttrRes int attrColor) { + final TypedValue value = new TypedValue(); + context.getTheme().resolveAttribute(attrColor, value, true); + + if (value.resourceId != 0) { + return ContextCompat.getColor(context, value.resourceId); + } + + return value.data; + } + + private static String getSelectedThemeString(Context context) { + String themeKey = context.getString(R.string.theme_key); + String defaultTheme = context.getResources().getString(R.string.default_theme_value); + return PreferenceManager.getDefaultSharedPreferences(context).getString(themeKey, defaultTheme); + } } diff --git a/app/src/main/java/org/schabi/newpipe/views/CollapsibleView.java b/app/src/main/java/org/schabi/newpipe/views/CollapsibleView.java new file mode 100644 index 000000000..adef7e76f --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/views/CollapsibleView.java @@ -0,0 +1,230 @@ +/* + * Copyright 2018 Mauricio Colli + * CollapsibleView.java is part of NewPipe + * + * License: GPL-3.0+ + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.schabi.newpipe.views; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.os.Build; +import android.os.Parcelable; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.LinearLayout; + +import org.schabi.newpipe.util.AnimationUtils; + +import java.lang.annotation.Retention; +import java.util.ArrayList; +import java.util.List; + +import icepick.Icepick; +import icepick.State; + +import static java.lang.annotation.RetentionPolicy.SOURCE; +import static org.schabi.newpipe.MainActivity.DEBUG; + +/** + * A view that can be fully collapsed and expanded. + */ +public class CollapsibleView extends LinearLayout { + private static final String TAG = CollapsibleView.class.getSimpleName(); + + public CollapsibleView(Context context) { + super(context); + } + + public CollapsibleView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public CollapsibleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public CollapsibleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + /*////////////////////////////////////////////////////////////////////////// + // Collapse/expand logic + //////////////////////////////////////////////////////////////////////////*/ + + private static final int ANIMATION_DURATION = 420; + public static final int COLLAPSED = 0, EXPANDED = 1; + + @Retention(SOURCE) + @IntDef({COLLAPSED, EXPANDED}) + public @interface ViewMode {} + + @State @ViewMode int currentState = COLLAPSED; + private boolean readyToChangeState; + + private int targetHeight = -1; + private ValueAnimator currentAnimator; + private List listeners = new ArrayList<>(); + + /** + * This method recalculates the height of this view so it must be called when + * some child changes (e.g. add new views, change text). + */ + public void ready() { + if (DEBUG) { + Log.d(TAG, getDebugLogString("ready() called")); + } + + measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), MeasureSpec.UNSPECIFIED); + targetHeight = getMeasuredHeight(); + + getLayoutParams().height = currentState == COLLAPSED ? 0 : targetHeight; + requestLayout(); + broadcastState(); + + readyToChangeState = true; + + if (DEBUG) { + Log.d(TAG, getDebugLogString("ready() *after* measuring")); + } + } + + public void collapse() { + if (DEBUG) { + Log.d(TAG, getDebugLogString("collapse() called")); + } + + if (!readyToChangeState) return; + + final int height = getHeight(); + if (height == 0) { + setCurrentState(COLLAPSED); + return; + } + + if (currentAnimator != null && currentAnimator.isRunning()) currentAnimator.cancel(); + currentAnimator = AnimationUtils.animateHeight(this, ANIMATION_DURATION, 0); + + setCurrentState(COLLAPSED); + } + + public void expand() { + if (DEBUG) { + Log.d(TAG, getDebugLogString("expand() called")); + } + + if (!readyToChangeState) return; + + final int height = getHeight(); + if (height == this.targetHeight) { + setCurrentState(EXPANDED); + return; + } + + if (currentAnimator != null && currentAnimator.isRunning()) currentAnimator.cancel(); + currentAnimator = AnimationUtils.animateHeight(this, ANIMATION_DURATION, this.targetHeight); + setCurrentState(EXPANDED); + } + + public void switchState() { + if (!readyToChangeState) return; + + if (currentState == COLLAPSED) { + expand(); + } else { + collapse(); + } + } + + @ViewMode + public int getCurrentState() { + return currentState; + } + + public void setCurrentState(@ViewMode int currentState) { + this.currentState = currentState; + broadcastState(); + } + + public void broadcastState() { + for (StateListener listener : listeners) { + listener.onStateChanged(currentState); + } + } + + /** + * Add a listener which will be listening for changes in this view (i.e. collapsed or expanded). + */ + public void addListener(final StateListener listener) { + if (listeners.contains(listener)) { + throw new IllegalStateException("Trying to add the same listener multiple times"); + } + + listeners.add(listener); + } + + /** + * Remove a listener so it doesn't receive more state changes. + */ + public void removeListener(final StateListener listener) { + listeners.remove(listener); + } + + /** + * Simple interface used for listening state changes of the {@link CollapsibleView}. + */ + public interface StateListener { + /** + * Called when the state changes. + * + * @param newState the state that the {@link CollapsibleView} transitioned to,
+ * it's an integer being either {@link #COLLAPSED} or {@link #EXPANDED} + */ + void onStateChanged(@ViewMode int newState); + } + + /*////////////////////////////////////////////////////////////////////////// + // State Saving + //////////////////////////////////////////////////////////////////////////*/ + + @Nullable + @Override + public Parcelable onSaveInstanceState() { + return Icepick.saveInstanceState(this, super.onSaveInstanceState()); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + super.onRestoreInstanceState(Icepick.restoreInstanceState(this, state)); + + ready(); + } + + /*////////////////////////////////////////////////////////////////////////// + // Internal + //////////////////////////////////////////////////////////////////////////*/ + + public String getDebugLogString(String description) { + return String.format("%-100s → %s", + description, "readyToChangeState = [" + readyToChangeState + "], currentState = [" + currentState + "], targetHeight = [" + targetHeight + "]," + + " mW x mH = [" + getMeasuredWidth() + "x" + getMeasuredHeight() + "]" + + " W x H = [" + getWidth() + "x" + getHeight() + "]"); + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_backup_black_24dp.png new file mode 100644 index 000000000..e0938f1dc Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_backup_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_backup_white_24dp.png new file mode 100644 index 000000000..5e0b464cf Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_backup_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_import_export_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_import_export_black_24dp.png new file mode 100644 index 000000000..b4466c849 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_import_export_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_import_export_white_24dp.png new file mode 100644 index 000000000..5b6c02010 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_import_export_white_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_save_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_save_black_24dp.png new file mode 100644 index 000000000..b959dc4a8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_save_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_save_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_save_white_24dp.png new file mode 100644 index 000000000..dd3f10664 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_save_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_backup_black_24dp.png new file mode 100644 index 000000000..4cd6741c0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_backup_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_backup_white_24dp.png new file mode 100644 index 000000000..aa640629a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_backup_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_import_export_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_import_export_black_24dp.png new file mode 100644 index 000000000..90f8c4567 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_import_export_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_import_export_white_24dp.png new file mode 100644 index 000000000..151188cf8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_import_export_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_save_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_save_black_24dp.png new file mode 100644 index 000000000..663479b73 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_save_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_save_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_save_white_24dp.png new file mode 100644 index 000000000..015062ed3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_save_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_backup_black_24dp.png new file mode 100644 index 000000000..81155da52 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_backup_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_backup_white_24dp.png new file mode 100644 index 000000000..a9602d11b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_backup_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_import_export_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_import_export_black_24dp.png new file mode 100644 index 000000000..9b643bd3b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_import_export_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png new file mode 100644 index 000000000..e22e18866 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_import_export_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_save_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_save_black_24dp.png new file mode 100644 index 000000000..eca2d92ec Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_save_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_save_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_save_white_24dp.png new file mode 100644 index 000000000..adda09575 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_save_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_backup_black_24dp.png new file mode 100644 index 000000000..6506c7236 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_backup_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_backup_white_24dp.png new file mode 100644 index 000000000..3ff57ad3e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_backup_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_import_export_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_import_export_black_24dp.png new file mode 100644 index 000000000..78e865dfa Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_import_export_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png new file mode 100644 index 000000000..33c21c5c4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_import_export_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_save_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_save_black_24dp.png new file mode 100644 index 000000000..871291b4e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_save_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_save_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_save_white_24dp.png new file mode 100644 index 000000000..3e0ce1a5f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_save_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_backup_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_backup_black_24dp.png new file mode 100644 index 000000000..248289e97 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_backup_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_backup_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_backup_white_24dp.png new file mode 100644 index 000000000..2180f73e8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_backup_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_import_export_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_import_export_black_24dp.png new file mode 100644 index 000000000..36aa872e5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_import_export_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png new file mode 100644 index 000000000..a5e55a470 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_import_export_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_save_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_save_black_24dp.png new file mode 100644 index 000000000..ba001835a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_save_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_save_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_save_white_24dp.png new file mode 100644 index 000000000..bd80bf1f7 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_save_white_24dp.png differ diff --git a/app/src/main/res/layout/fragment_import.xml b/app/src/main/res/layout/fragment_import.xml new file mode 100644 index 000000000..b75a5d487 --- /dev/null +++ b/app/src/main/res/layout/fragment_import.xml @@ -0,0 +1,55 @@ + + + + + + + + + +