/* * Copyright 2016 Hippo Seven * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.hippo.ehviewer.ui; import android.Manifest; import android.animation.Animator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.app.assist.AssistContent; import android.content.ClipData; import android.content.ClipboardManager; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Typeface; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.provider.MediaStore; import android.text.TextUtils; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.webkit.MimeTypeMap; import android.widget.FrameLayout; import android.widget.SeekBar; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.core.view.ViewCompat; import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsControllerCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.materialswitch.MaterialSwitch; import com.hippo.ehviewer.AppConfig; import com.hippo.ehviewer.BuildConfig; import com.hippo.ehviewer.R; import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.client.EhUrl; import com.hippo.ehviewer.client.data.GalleryInfo; import com.hippo.ehviewer.gallery.ArchiveGalleryProvider; import com.hippo.ehviewer.gallery.DirGalleryProvider; import com.hippo.ehviewer.gallery.EhGalleryProvider; import com.hippo.ehviewer.gallery.GalleryProvider2; import com.hippo.ehviewer.widget.GalleryGuideView; import com.hippo.ehviewer.widget.GalleryHeader; import com.hippo.ehviewer.widget.ReversibleSeekBar; import com.hippo.glgallery.GalleryProvider; import com.hippo.glgallery.GalleryView; import com.hippo.glgallery.SimpleAdapter; import com.hippo.glview.view.GLRootView; import com.hippo.unifile.UniFile; import com.hippo.util.ExceptionUtils; import com.hippo.util.IoThreadPoolExecutor; import com.hippo.widget.ColorView; import com.hippo.yorozuya.AnimationUtils; import com.hippo.yorozuya.ConcurrentPool; import com.hippo.yorozuya.FileUtils; import com.hippo.yorozuya.IOUtils; import com.hippo.yorozuya.MathUtils; import com.hippo.yorozuya.ResourcesUtils; import com.hippo.yorozuya.SimpleAnimatorListener; import com.hippo.yorozuya.SimpleHandler; import com.hippo.yorozuya.ViewUtils; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import rikka.core.res.ConfigurationKt; import rikka.core.res.ResourcesKt; import rikka.material.app.DayNightDelegate; public class GalleryActivity extends EhActivity implements SeekBar.OnSeekBarChangeListener, GalleryView.Listener { public static final String ACTION_DIR = "dir"; public static final String ACTION_EH = "eh"; public static final String KEY_ACTION = "action"; public static final String KEY_FILENAME = "filename"; public static final String KEY_URI = "uri"; public static final String KEY_GALLERY_INFO = "gallery_info"; public static final String KEY_PAGE = "page"; public static final String KEY_CURRENT_INDEX = "current_index"; private static final long SLIDER_ANIMATION_DURING = 150; private static final long HIDE_SLIDER_DELAY = 3000; private final ConcurrentPool mNotifyTaskPool = new ConcurrentPool<>(3); private String mAction; private String mFilename; private Uri mUri; private GalleryInfo mGalleryInfo; private int mPage; private String mCacheFileName; ActivityResultLauncher saveImageToLauncher = registerForActivityResult( new ActivityResultContracts.CreateDocument(), uri -> { if (uri != null) { String filepath = AppConfig.getExternalTempDir() + File.separator + mCacheFileName; File cachefile = new File(filepath); ContentResolver resolver = getContentResolver(); IoThreadPoolExecutor.getInstance().execute(() -> { InputStream is = null; OutputStream os = null; try { is = new FileInputStream(cachefile); os = resolver.openOutputStream(uri); IOUtils.copy(is, os); } catch (IOException e) { e.printStackTrace(); } finally { IOUtils.closeQuietly(is); IOUtils.closeQuietly(os); runOnUiThread(() -> Toast.makeText(GalleryActivity.this, getString(R.string.image_saved, uri.getPath()), Toast.LENGTH_SHORT).show()); } //noinspection ResultOfMethodCallIgnored cachefile.delete(); }); } }); @Nullable private GLRootView mGLRootView; @Nullable private GalleryView mGalleryView; @Nullable private GalleryProvider2 mGalleryProvider; @Nullable private GalleryAdapter mGalleryAdapter; @Nullable private WindowInsetsControllerCompat insetsController; @Nullable private ColorView mMaskView; @Nullable private View mClock; @Nullable private TextView mProgress; @Nullable private View mBattery; @Nullable private View mSeekBarPanel; private final ValueAnimator.AnimatorUpdateListener mUpdateSliderListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { if (null != mSeekBarPanel) { mSeekBarPanel.requestLayout(); } } }; @Nullable private TextView mLeftText; @Nullable private TextView mRightText; @Nullable private ReversibleSeekBar mSeekBar; private ObjectAnimator mSeekBarPanelAnimator; private final SimpleAnimatorListener mShowSliderListener = new SimpleAnimatorListener() { @Override public void onAnimationEnd(Animator animation) { mSeekBarPanelAnimator = null; } }; private final SimpleAnimatorListener mHideSliderListener = new SimpleAnimatorListener() { @Override public void onAnimationEnd(Animator animation) { mSeekBarPanelAnimator = null; if (mSeekBarPanel != null) { mSeekBarPanel.setVisibility(View.INVISIBLE); } } }; private final Runnable mHideSliderRunnable = new Runnable() { @Override public void run() { if (mSeekBarPanel != null) { hideSlider(mSeekBarPanel); } } }; private int mLayoutMode; private int mSize; private int mCurrentIndex; private int mSavingPage = -1; private void buildProvider() { if (mGalleryProvider != null) { return; } if (ACTION_DIR.equals(mAction)) { if (mFilename != null) { mGalleryProvider = new DirGalleryProvider(UniFile.fromFile(new File(mFilename))); } } else if (ACTION_EH.equals(mAction)) { if (mGalleryInfo != null) { mGalleryProvider = new EhGalleryProvider(this, mGalleryInfo); } } else if (Intent.ACTION_VIEW.equals(mAction)) { if (mUri != null) { try { grantUriPermission(BuildConfig.APPLICATION_ID, mUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); } catch (Exception e) { Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show(); } mGalleryProvider = new ArchiveGalleryProvider(this, mUri); } } } private void onInit() { Intent intent = getIntent(); if (intent == null) { return; } mAction = intent.getAction(); mFilename = intent.getStringExtra(KEY_FILENAME); mUri = intent.getData(); mGalleryInfo = intent.getParcelableExtra(KEY_GALLERY_INFO); mPage = intent.getIntExtra(KEY_PAGE, -1); buildProvider(); } private void onRestore(@NonNull Bundle savedInstanceState) { mAction = savedInstanceState.getString(KEY_ACTION); mFilename = savedInstanceState.getString(KEY_FILENAME); mUri = savedInstanceState.getParcelable(KEY_URI); mGalleryInfo = savedInstanceState.getParcelable(KEY_GALLERY_INFO); mPage = savedInstanceState.getInt(KEY_PAGE, -1); mCurrentIndex = savedInstanceState.getInt(KEY_CURRENT_INDEX); buildProvider(); } ActivityResultLauncher requestStoragePermissionLauncher = registerForActivityResult( new ActivityResultContracts.RequestPermission(), result -> { if (result && mSavingPage != -1) { saveImage(mSavingPage); } else { Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); } mSavingPage = -1; }); @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putString(KEY_ACTION, mAction); outState.putString(KEY_FILENAME, mFilename); outState.putParcelable(KEY_URI, mUri); if (mGalleryInfo != null) { outState.putParcelable(KEY_GALLERY_INFO, mGalleryInfo); } outState.putInt(KEY_PAGE, mPage); outState.putInt(KEY_CURRENT_INDEX, mCurrentIndex); } @Override protected void attachBaseContext(@NonNull Context newBase) { switch (Settings.getReadTheme()) { case 1: getDayNightDelegate().setLocalNightMode(DayNightDelegate.MODE_NIGHT_YES, false); break; case 2: getDayNightDelegate().setLocalNightMode(DayNightDelegate.MODE_NIGHT_NO, false); break; } super.attachBaseContext(newBase); } @Override public boolean respectDefaultNightMode() { return Settings.getReadTheme() == 0; } @Override @SuppressWarnings({"WrongConstant"}) protected void onCreate(@Nullable Bundle savedInstanceState) { if (Settings.getReadingFullscreen()) { Window w = getWindow(); w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); } super.onCreate(savedInstanceState); if (savedInstanceState == null) { onInit(); } else { onRestore(savedInstanceState); } if (mGalleryProvider == null) { finish(); return; } mGalleryProvider.start(); // Get start page int startPage; if (savedInstanceState == null) { startPage = mPage >= 0 ? mPage : mGalleryProvider.getStartPage(); } else { startPage = mCurrentIndex; } setContentView(R.layout.activity_gallery); mGLRootView = (GLRootView) ViewUtils.$$(this, R.id.gl_root_view); mGalleryAdapter = new GalleryAdapter(mGLRootView, mGalleryProvider); Resources resources = getResources(); mGalleryView = new GalleryView.Builder(this, mGalleryAdapter) .setListener(this) .setLayoutMode(Settings.getReadingDirection()) .setScaleMode(Settings.getPageScaling()) .setStartPosition(Settings.getStartPosition()) .setStartPage(startPage) .setBackgroundColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.colorBackground)) .setEdgeColor(ResourcesKt.resolveColor(getTheme(), R.attr.colorEdgeEffect) & 0xffffff | 0x33000000) .setPagerInterval(Settings.getShowPageInterval() ? resources.getDimensionPixelOffset(R.dimen.gallery_pager_interval) : 0) .setScrollInterval(Settings.getShowPageInterval() ? resources.getDimensionPixelOffset(R.dimen.gallery_scroll_interval) : 0) .setPageMinHeight(resources.getDimensionPixelOffset(R.dimen.gallery_page_min_height)) .setPageInfoInterval(resources.getDimensionPixelOffset(R.dimen.gallery_page_info_interval)) .setProgressColor(ResourcesUtils.getAttrColor(this, androidx.appcompat.R.attr.colorPrimary)) .setProgressSize(resources.getDimensionPixelOffset(R.dimen.gallery_progress_size)) .setPageTextColor(ResourcesKt.resolveColor(getTheme(), android.R.attr.textColorSecondary)) .setPageTextSize(resources.getDimensionPixelOffset(R.dimen.gallery_page_text_size)) .setPageTextTypeface(Typeface.DEFAULT) .setErrorTextColor(resources.getColor(R.color.red_500)) .setErrorTextSize(resources.getDimensionPixelOffset(R.dimen.gallery_error_text_size)) .setDefaultErrorString(resources.getString(R.string.error_unknown)) .setEmptyString(resources.getString(R.string.error_empty)) .build(); mGLRootView.setContentPane(mGalleryView); mGalleryProvider.setListener(mGalleryAdapter); mGalleryProvider.setGLRoot(mGLRootView); WindowCompat.setDecorFitsSystemWindows(getWindow(), false); insetsController = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()); if (insetsController != null) { if (Settings.getReadingFullscreen()) { insetsController.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); // This does not hide navigation bar sometimes on 31, framework bug??? insetsController.hide(WindowInsetsCompat.Type.systemBars()); } else { insetsController.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH); insetsController.show(WindowInsetsCompat.Type.systemBars()); } boolean night = ConfigurationKt.isNight(getResources().getConfiguration()); insetsController.setAppearanceLightStatusBars(!night); insetsController.setAppearanceLightNavigationBars(!night); } mMaskView = (ColorView) ViewUtils.$$(this, R.id.mask); mMaskView.setOnGenericMotionListener((view, event) -> { if (mGalleryView == null) { return false; } switch (event.getAction()) { case MotionEvent.ACTION_SCROLL: if (event.getAxisValue(MotionEvent.AXIS_VSCROLL) < 0.0f) { if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { mGalleryView.pageLeft(); } else { mGalleryView.pageRight(); } } else { if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { mGalleryView.pageRight(); } else { mGalleryView.pageLeft(); } } } return false; }); mClock = ViewUtils.$$(this, R.id.clock); mProgress = (TextView) ViewUtils.$$(this, R.id.progress); mBattery = ViewUtils.$$(this, R.id.battery); mClock.setVisibility(Settings.getShowClock() ? View.VISIBLE : View.GONE); mProgress.setVisibility(Settings.getShowProgress() ? View.VISIBLE : View.GONE); mBattery.setVisibility(Settings.getShowBattery() ? View.VISIBLE : View.GONE); mSeekBarPanel = ViewUtils.$$(this, R.id.seek_bar_panel); mLeftText = (TextView) ViewUtils.$$(mSeekBarPanel, R.id.left); mRightText = (TextView) ViewUtils.$$(mSeekBarPanel, R.id.right); mSeekBar = (ReversibleSeekBar) ViewUtils.$$(mSeekBarPanel, R.id.seek_bar); mSeekBar.setOnSeekBarChangeListener(this); mSize = mGalleryProvider.size(); mCurrentIndex = startPage; if (mGalleryView != null) { mLayoutMode = mGalleryView.getLayoutMode(); } updateSlider(); // Update keep screen on if (Settings.getKeepScreenOn()) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } // Orientation int orientation; switch (Settings.getScreenRotation()) { default: case 0: orientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; break; case 1: orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; break; case 2: orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; break; case 3: orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR; break; } setRequestedOrientation(orientation); // Screen lightness setScreenLightness(Settings.getCustomScreenLightness(), Settings.getScreenLightness()); // Cutout if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; } GalleryHeader galleryHeader = findViewById(R.id.gallery_header); ViewCompat.setOnApplyWindowInsetsListener(galleryHeader, (v, insets) -> { if (!Settings.getReadingFullscreen()) { galleryHeader.setTopInsets(insets.getInsets(WindowInsetsCompat.Type.statusBars()).top); } else { galleryHeader.setDisplayCutout(insets.getDisplayCutout()); } return WindowInsetsCompat.CONSUMED; }); if (Settings.getGuideGallery()) { FrameLayout mainLayout = (FrameLayout) ViewUtils.$$(this, R.id.main); mainLayout.addView(new GalleryGuideView(this)); } } @Override protected void onDestroy() { super.onDestroy(); mGLRootView = null; mGalleryView = null; if (mGalleryAdapter != null) { mGalleryAdapter.clearUploader(); mGalleryAdapter = null; } if (mGalleryProvider != null) { mGalleryProvider.setListener(null); mGalleryProvider.stop(); mGalleryProvider = null; } mMaskView = null; mClock = null; mProgress = null; mBattery = null; mSeekBarPanel = null; mLeftText = null; mRightText = null; mSeekBar = null; SimpleHandler.getInstance().removeCallbacks(mHideSliderRunnable); } @Override protected void onPause() { super.onPause(); if (mGLRootView != null) { mGLRootView.onPause(); } } @Override protected void onResume() { super.onResume(); if (mGLRootView != null) { mGLRootView.onResume(); } } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (mGalleryView == null) { return super.onKeyDown(keyCode, event); } // Check volume if (Settings.getVolumePage()) { if (Settings.getReverseVolumePage()) { if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { keyCode = KeyEvent.KEYCODE_VOLUME_DOWN; } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { keyCode = KeyEvent.KEYCODE_VOLUME_UP; } } if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { mGalleryView.pageRight(); } else { mGalleryView.pageLeft(); } return true; } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { mGalleryView.pageLeft(); } else { mGalleryView.pageRight(); } return true; } } // Check keyboard and Dpad switch (keyCode) { case KeyEvent.KEYCODE_PAGE_UP: case KeyEvent.KEYCODE_DPAD_UP: if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { mGalleryView.pageRight(); } else { mGalleryView.pageLeft(); } return true; case KeyEvent.KEYCODE_DPAD_LEFT: mGalleryView.pageLeft(); return true; case KeyEvent.KEYCODE_PAGE_DOWN: case KeyEvent.KEYCODE_DPAD_DOWN: if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { mGalleryView.pageLeft(); } else { mGalleryView.pageRight(); } return true; case KeyEvent.KEYCODE_DPAD_RIGHT: mGalleryView.pageRight(); return true; case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_SPACE: case KeyEvent.KEYCODE_MENU: onTapMenuArea(); return true; } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { // Check volume if (Settings.getVolumePage()) { if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { return true; } } // Check keyboard and Dpad if (keyCode == KeyEvent.KEYCODE_PAGE_UP || keyCode == KeyEvent.KEYCODE_PAGE_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_SPACE || keyCode == KeyEvent.KEYCODE_MENU) { return true; } return super.onKeyUp(keyCode, event); } @SuppressLint("SetTextI18n") private void updateProgress() { if (mProgress == null) { return; } if (mSize <= 0 || mCurrentIndex < 0) { mProgress.setText(null); } else { mProgress.setText((mCurrentIndex + 1) + "/" + mSize); } } @SuppressLint("SetTextI18n") private void updateSlider() { if (mSeekBar == null || mRightText == null || mLeftText == null || mSize <= 0 || mCurrentIndex < 0) { return; } TextView start; TextView end; if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { start = mRightText; end = mLeftText; mSeekBar.setReverse(true); } else { start = mLeftText; end = mRightText; mSeekBar.setReverse(false); } start.setText(Integer.toString(mCurrentIndex + 1)); end.setText(Integer.toString(mSize)); mSeekBar.setMax(mSize - 1); mSeekBar.setProgress(mCurrentIndex); } @Override @SuppressLint("SetTextI18n") public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { TextView start; if (mLayoutMode == GalleryView.LAYOUT_RIGHT_TO_LEFT) { start = mRightText; } else { start = mLeftText; } if (fromUser && null != start) { start.setText(Integer.toString(progress + 1)); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { SimpleHandler.getInstance().removeCallbacks(mHideSliderRunnable); } @Override public void onStopTrackingTouch(SeekBar seekBar) { SimpleHandler.getInstance().postDelayed(mHideSliderRunnable, HIDE_SLIDER_DELAY); int progress = seekBar.getProgress(); if (progress != mCurrentIndex && null != mGalleryView) { mGalleryView.setCurrentPage(progress); } } @Override public void onUpdateCurrentIndex(int index) { if (null != mGalleryProvider) { mGalleryProvider.putStartPage(index); } NotifyTask task = mNotifyTaskPool.pop(); if (task == null) { task = new NotifyTask(); } task.setData(NotifyTask.KEY_CURRENT_INDEX, index); SimpleHandler.getInstance().post(task); } @Override public void onTapSliderArea() { NotifyTask task = mNotifyTaskPool.pop(); if (task == null) { task = new NotifyTask(); } task.setData(NotifyTask.KEY_TAP_SLIDER_AREA, 0); SimpleHandler.getInstance().post(task); } @Override public void onTapMenuArea() { NotifyTask task = mNotifyTaskPool.pop(); if (task == null) { task = new NotifyTask(); } task.setData(NotifyTask.KEY_TAP_MENU_AREA, 0); SimpleHandler.getInstance().post(task); } @Override public void onTapErrorText(int index) { NotifyTask task = mNotifyTaskPool.pop(); if (task == null) { task = new NotifyTask(); } task.setData(NotifyTask.KEY_TAP_ERROR_TEXT, index); SimpleHandler.getInstance().post(task); } @Override public void onLongPressPage(int index) { NotifyTask task = mNotifyTaskPool.pop(); if (task == null) { task = new NotifyTask(); } task.setData(NotifyTask.KEY_LONG_PRESS_PAGE, index); SimpleHandler.getInstance().post(task); } private void showSlider(View sliderPanel) { if (null != mSeekBarPanelAnimator) { mSeekBarPanelAnimator.cancel(); mSeekBarPanelAnimator = null; } sliderPanel.setTranslationY(sliderPanel.getHeight()); sliderPanel.setVisibility(View.VISIBLE); mSeekBarPanelAnimator = ObjectAnimator.ofFloat(sliderPanel, "translationY", 0.0f); mSeekBarPanelAnimator.setDuration(SLIDER_ANIMATION_DURING); mSeekBarPanelAnimator.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR); mSeekBarPanelAnimator.addUpdateListener(mUpdateSliderListener); mSeekBarPanelAnimator.addListener(mShowSliderListener); mSeekBarPanelAnimator.start(); if (insetsController != null) { if (Settings.getReadingFullscreen()) insetsController.show(WindowInsetsCompat.Type.systemBars()); } } private void hideSlider(View sliderPanel) { if (null != mSeekBarPanelAnimator) { mSeekBarPanelAnimator.cancel(); mSeekBarPanelAnimator = null; } mSeekBarPanelAnimator = ObjectAnimator.ofFloat(sliderPanel, "translationY", sliderPanel.getHeight()); mSeekBarPanelAnimator.setDuration(SLIDER_ANIMATION_DURING); mSeekBarPanelAnimator.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR); mSeekBarPanelAnimator.addUpdateListener(mUpdateSliderListener); mSeekBarPanelAnimator.addListener(mHideSliderListener); mSeekBarPanelAnimator.start(); if (insetsController != null) { if (Settings.getReadingFullscreen()) insetsController.hide(WindowInsetsCompat.Type.systemBars()); } } /** * @param lightness 0 - 200 */ private void setScreenLightness(boolean enable, int lightness) { if (null == mMaskView) { return; } Window w = getWindow(); WindowManager.LayoutParams lp = w.getAttributes(); if (enable) { lightness = MathUtils.clamp(lightness, 0, 200); if (lightness > 100) { mMaskView.setColor(0); // Avoid BRIGHTNESS_OVERRIDE_OFF, // screen may be off when lp.screenBrightness is 0.0f lp.screenBrightness = Math.max((lightness - 100) / 100.0f, 0.01f); } else { mMaskView.setColor(MathUtils.lerp(0xde, 0x00, lightness / 100.0f) << 24); lp.screenBrightness = 0.01f; } } else { mMaskView.setColor(0); lp.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE; } w.setAttributes(lp); } private void shareImage(int page) { if (null == mGalleryProvider) { return; } File dir = AppConfig.getExternalTempDir(); if (null == dir) { Toast.makeText(this, R.string.error_cant_create_temp_file, Toast.LENGTH_SHORT).show(); return; } UniFile file; if (null == (file = mGalleryProvider.save(page, UniFile.fromFile(dir), mGalleryProvider.getImageFilename(page)))) { Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); return; } String filename = file.getName(); if (filename == null) { Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); return; } String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( MimeTypeMap.getFileExtensionFromUrl(filename)); if (TextUtils.isEmpty(mimeType)) { mimeType = "image/jpeg"; } Uri uri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", new File(dir, filename)); Intent intent = new Intent(); intent.setAction(Intent.ACTION_SEND); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.putExtra(Intent.EXTRA_STREAM, uri); intent.putExtra(Intent.EXTRA_TEXT, EhUrl.getGalleryDetailUrl(mGalleryInfo.gid, mGalleryInfo.token)); intent.setDataAndType(uri, mimeType); try { startActivity(Intent.createChooser(intent, getString(R.string.share_image))); } catch (Throwable e) { ExceptionUtils.throwIfFatal(e); Toast.makeText(this, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show(); } } private void copyImage(int page) { if (null == mGalleryProvider) { return; } File dir = AppConfig.getExternalCopyTempDir(); if (null == dir) { Toast.makeText(this, R.string.error_cant_create_temp_file, Toast.LENGTH_SHORT).show(); return; } UniFile file; if (null == (file = mGalleryProvider.save(page, UniFile.fromFile(dir), mGalleryProvider.getImageFilename(page)))) { Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); return; } String filename = file.getName(); if (filename == null) { Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); return; } Uri uri = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", new File(dir, filename)); var clipboardManager = getSystemService(ClipboardManager.class); if (clipboardManager != null) { var clipData = ClipData.newUri(getContentResolver(), "ehviewer", uri); clipboardManager.setPrimaryClip(clipData); Toast.makeText(this, getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT).show(); } } private void saveImage(int page) { if (null == mGalleryProvider) { return; } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { mSavingPage = page; requestStoragePermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE); return; } String filename = mGalleryProvider.getImageFilenameWithExtension(page); String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( MimeTypeMap.getFileExtensionFromUrl(filename)); if (TextUtils.isEmpty(mimeType)) { mimeType = "image/jpeg"; } String realPath; ContentResolver resolver = getContentResolver(); ContentValues values = new ContentValues(); values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename); values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()); values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME); values.put(MediaStore.MediaColumns.IS_PENDING, 1); realPath = Environment.DIRECTORY_PICTURES + File.separator + AppConfig.APP_DIRNAME; } else { File path = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), AppConfig.APP_DIRNAME); realPath = path.toString(); if (!FileUtils.ensureDirectory(path)) { Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); return; } values.put(MediaStore.MediaColumns.DATA, path + File.separator + filename); } Uri imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); if (imageUri == null) { Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); return; } if (!mGalleryProvider.save(page, UniFile.fromMediaUri(this, imageUri))) { try { resolver.delete(imageUri, null, null); } catch (Exception e) { e.printStackTrace(); } Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); return; } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ContentValues contentValues = new ContentValues(); contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0); resolver.update(imageUri, contentValues, null, null); } Toast.makeText(this, getString(R.string.image_saved, realPath + File.separator + filename), Toast.LENGTH_SHORT).show(); } private void saveImageTo(int page) { if (null == mGalleryProvider) { return; } File dir = AppConfig.getExternalTempDir(); if (null == dir) { Toast.makeText(this, R.string.error_cant_create_temp_file, Toast.LENGTH_SHORT).show(); return; } UniFile file; if (null == (file = mGalleryProvider.save(page, UniFile.fromFile(dir), mGalleryProvider.getImageFilename(page)))) { Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); return; } String filename = file.getName(); if (filename == null) { Toast.makeText(this, R.string.error_cant_save_image, Toast.LENGTH_SHORT).show(); return; } mCacheFileName = filename; try { saveImageToLauncher.launch(filename); } catch (Throwable e) { ExceptionUtils.throwIfFatal(e); Toast.makeText(this, R.string.error_cant_find_activity, Toast.LENGTH_SHORT).show(); } } private void showPageDialog(final int page) { Resources resources = GalleryActivity.this.getResources(); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(GalleryActivity.this); builder.setTitle(resources.getString(R.string.page_menu_title, page + 1)); final CharSequence[] items; items = new CharSequence[]{ getString(R.string.page_menu_refresh), getString(R.string.page_menu_share), getString(android.R.string.copy), getString(R.string.page_menu_save), getString(R.string.page_menu_save_to)}; pageDialogListener(builder, items, page); builder.show(); } private void pageDialogListener(AlertDialog.Builder builder, CharSequence[] items, int page) { builder.setItems(items, (dialog, which) -> { if (mGalleryProvider == null) { return; } switch (which) { case 0: // Refresh mGalleryProvider.removeCache(page); mGalleryProvider.forceRequest(page); break; case 1: // Share shareImage(page); break; case 2: // Copy copyImage(page); break; case 3: // Save saveImage(page); break; case 4: // Save to saveImageTo(page); break; } }); } @Nullable private String getGalleryDetailUrl() { long gid; String token; if (mGalleryInfo != null) { gid = mGalleryInfo.gid; token = mGalleryInfo.token; } else { return null; } return EhUrl.getGalleryDetailUrl(gid, token, 0, false); } @Override public void onProvideAssistContent(AssistContent outContent) { super.onProvideAssistContent(outContent); String url = getGalleryDetailUrl(); if (url != null) { outContent.setWebUri(Uri.parse(url)); } } private class GalleryMenuHelper implements DialogInterface.OnClickListener { private final View mView; private final Spinner mScreenRotation; private final Spinner mReadingDirection; private final Spinner mScaleMode; private final Spinner mStartPosition; private final Spinner mReadTheme; private final MaterialSwitch mKeepScreenOn; private final MaterialSwitch mShowClock; private final MaterialSwitch mShowProgress; private final MaterialSwitch mShowBattery; private final MaterialSwitch mShowPageInterval; private final MaterialSwitch mVolumePage; private final MaterialSwitch mReverseVolumePage; private final MaterialSwitch mReadingFullscreen; private final MaterialSwitch mCustomScreenLightness; private final SeekBar mScreenLightness; @SuppressLint("InflateParams") public GalleryMenuHelper(Context context) { mView = LayoutInflater.from(context).inflate(R.layout.dialog_gallery_menu, null); mScreenRotation = mView.findViewById(R.id.screen_rotation); mReadingDirection = mView.findViewById(R.id.reading_direction); mScaleMode = mView.findViewById(R.id.page_scaling); mStartPosition = mView.findViewById(R.id.start_position); mReadTheme = mView.findViewById(R.id.read_theme); mKeepScreenOn = mView.findViewById(R.id.keep_screen_on); mShowClock = mView.findViewById(R.id.show_clock); mShowProgress = mView.findViewById(R.id.show_progress); mShowBattery = mView.findViewById(R.id.show_battery); mShowPageInterval = mView.findViewById(R.id.show_page_interval); mVolumePage = mView.findViewById(R.id.volume_page); mReverseVolumePage = mView.findViewById(R.id.reverse_volume_page); mReadingFullscreen = mView.findViewById(R.id.reading_fullscreen); mCustomScreenLightness = mView.findViewById(R.id.custom_screen_lightness); mScreenLightness = mView.findViewById(R.id.screen_lightness); mScreenRotation.setSelection(Settings.getScreenRotation()); mReadingDirection.setSelection(Settings.getReadingDirection()); mScaleMode.setSelection(Settings.getPageScaling()); mStartPosition.setSelection(Settings.getStartPosition()); mReadTheme.setSelection(Settings.getReadTheme()); mKeepScreenOn.setChecked(Settings.getKeepScreenOn()); mShowClock.setChecked(Settings.getShowClock()); mShowProgress.setChecked(Settings.getShowProgress()); mShowBattery.setChecked(Settings.getShowBattery()); mShowPageInterval.setChecked(Settings.getShowPageInterval()); mVolumePage.setChecked(Settings.getVolumePage()); mVolumePage.setOnCheckedChangeListener((buttonView, isChecked) -> mReverseVolumePage.setEnabled(isChecked)); mReverseVolumePage.setEnabled(mVolumePage.isChecked()); mReverseVolumePage.setChecked(Settings.getReverseVolumePage()); mReadingFullscreen.setChecked(Settings.getReadingFullscreen()); mCustomScreenLightness.setChecked(Settings.getCustomScreenLightness()); mScreenLightness.setProgress(Settings.getScreenLightness()); mScreenLightness.setEnabled(Settings.getCustomScreenLightness()); mCustomScreenLightness.setOnCheckedChangeListener((buttonView, isChecked) -> mScreenLightness.setEnabled(isChecked)); } public View getView() { return mView; } @Override public void onClick(DialogInterface dialog, int which) { if (mGalleryView == null) { return; } int screenRotation = mScreenRotation.getSelectedItemPosition(); int layoutMode = GalleryView.sanitizeLayoutMode(mReadingDirection.getSelectedItemPosition()); int scaleMode = GalleryView.sanitizeScaleMode(mScaleMode.getSelectedItemPosition()); int startPosition = GalleryView.sanitizeStartPosition(mStartPosition.getSelectedItemPosition()); int readTheme = mReadTheme.getSelectedItemPosition(); boolean keepScreenOn = mKeepScreenOn.isChecked(); boolean showClock = mShowClock.isChecked(); boolean showProgress = mShowProgress.isChecked(); boolean showBattery = mShowBattery.isChecked(); boolean showPageInterval = mShowPageInterval.isChecked(); boolean volumePage = mVolumePage.isChecked(); boolean reverseVolumePage = mReverseVolumePage.isChecked(); boolean readingFullscreen = mReadingFullscreen.isChecked(); boolean customScreenLightness = mCustomScreenLightness.isChecked(); int screenLightness = mScreenLightness.getProgress(); boolean oldReadingFullscreen = Settings.getReadingFullscreen(); int oldReadTheme = Settings.getReadTheme(); Settings.putScreenRotation(screenRotation); Settings.putReadingDirection(layoutMode); Settings.putPageScaling(scaleMode); Settings.putStartPosition(startPosition); Settings.putReadTheme(readTheme); Settings.putKeepScreenOn(keepScreenOn); Settings.putShowClock(showClock); Settings.putShowProgress(showProgress); Settings.putShowBattery(showBattery); Settings.putShowPageInterval(showPageInterval); Settings.putVolumePage(volumePage); Settings.putReverseVolumePage(reverseVolumePage); Settings.putReadingFullscreen(readingFullscreen); Settings.putCustomScreenLightness(customScreenLightness); Settings.putScreenLightness(screenLightness); int orientation; switch (screenRotation) { default: case 0: orientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; break; case 1: orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; break; case 2: orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; break; case 3: orientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR; break; } setRequestedOrientation(orientation); mGalleryView.setLayoutMode(layoutMode); mGalleryView.setScaleMode(scaleMode); mGalleryView.setStartPosition(startPosition); if (keepScreenOn) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } if (mClock != null) { mClock.setVisibility(showClock ? View.VISIBLE : View.GONE); } if (mProgress != null) { mProgress.setVisibility(showProgress ? View.VISIBLE : View.GONE); } if (mBattery != null) { mBattery.setVisibility(showBattery ? View.VISIBLE : View.GONE); } mGalleryView.setPagerInterval(showPageInterval ? getResources().getDimensionPixelOffset(R.dimen.gallery_pager_interval) : 0); mGalleryView.setScrollInterval(showPageInterval ? getResources().getDimensionPixelOffset(R.dimen.gallery_scroll_interval) : 0); setScreenLightness(customScreenLightness, screenLightness); // Update slider mLayoutMode = layoutMode; updateSlider(); if (oldReadingFullscreen != readingFullscreen || oldReadTheme != readTheme) { recreate(); } } } private class NotifyTask implements Runnable { public static final int KEY_LAYOUT_MODE = 0; public static final int KEY_SIZE = 1; public static final int KEY_CURRENT_INDEX = 2; public static final int KEY_TAP_SLIDER_AREA = 3; public static final int KEY_TAP_MENU_AREA = 4; public static final int KEY_TAP_ERROR_TEXT = 5; public static final int KEY_LONG_PRESS_PAGE = 6; private int mKey; private int mValue; public void setData(int key, int value) { mKey = key; mValue = value; } private void onTapMenuArea() { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(GalleryActivity.this); GalleryMenuHelper helper = new GalleryMenuHelper(builder.getContext()); builder.setTitle(R.string.gallery_menu_title) .setView(helper.getView()) .setPositiveButton(android.R.string.ok, helper).show(); } private void onTapSliderArea() { if (mSeekBarPanel == null || mSize <= 0 || mCurrentIndex < 0) { return; } SimpleHandler.getInstance().removeCallbacks(mHideSliderRunnable); if (mSeekBarPanel.getVisibility() == View.VISIBLE) { hideSlider(mSeekBarPanel); } else { showSlider(mSeekBarPanel); SimpleHandler.getInstance().postDelayed(mHideSliderRunnable, HIDE_SLIDER_DELAY); } } private void onTapErrorText(int index) { if (mGalleryProvider != null) { mGalleryProvider.forceRequest(index); } } private void onLongPressPage(final int index) { showPageDialog(index); } @Override public void run() { switch (mKey) { case KEY_LAYOUT_MODE: GalleryActivity.this.mLayoutMode = mValue; updateSlider(); break; case KEY_SIZE: GalleryActivity.this.mSize = mValue; updateSlider(); updateProgress(); break; case KEY_CURRENT_INDEX: GalleryActivity.this.mCurrentIndex = mValue; updateSlider(); updateProgress(); break; case KEY_TAP_MENU_AREA: onTapMenuArea(); break; case KEY_TAP_SLIDER_AREA: onTapSliderArea(); break; case KEY_TAP_ERROR_TEXT: onTapErrorText(mValue); break; case KEY_LONG_PRESS_PAGE: onLongPressPage(mValue); break; } mNotifyTaskPool.push(this); } } private class GalleryAdapter extends SimpleAdapter { public GalleryAdapter(@NonNull GLRootView glRootView, @NonNull GalleryProvider provider) { super(glRootView, provider); } @Override public void onDataChanged() { super.onDataChanged(); if (mGalleryProvider != null) { int size = mGalleryProvider.size(); NotifyTask task = mNotifyTaskPool.pop(); if (task == null) { task = new NotifyTask(); } task.setData(NotifyTask.KEY_SIZE, size); SimpleHandler.getInstance().post(task); } } } }