diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index be3fc334a..ab240ef39 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -172,8 +172,11 @@ = 26 && context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); + } } diff --git a/app/src/main/java/de/xikolo/controllers/base/BaseActivity.java b/app/src/main/java/de/xikolo/controllers/base/BaseActivity.java index c4d90a581..fd9013aed 100644 --- a/app/src/main/java/de/xikolo/controllers/base/BaseActivity.java +++ b/app/src/main/java/de/xikolo/controllers/base/BaseActivity.java @@ -137,6 +137,9 @@ public boolean onCreateOptionsMenu(Menu menu) { protected void onNewIntent(Intent intent) { super.onNewIntent(intent); + setIntent(intent); + AutoBundle.bind(this); + handleIntent(intent); } diff --git a/app/src/main/java/de/xikolo/controllers/base/BasePresenterActivity.java b/app/src/main/java/de/xikolo/controllers/base/BasePresenterActivity.java index 39aabf16d..294e3e593 100644 --- a/app/src/main/java/de/xikolo/controllers/base/BasePresenterActivity.java +++ b/app/src/main/java/de/xikolo/controllers/base/BasePresenterActivity.java @@ -1,6 +1,7 @@ package de.xikolo.controllers.base; +import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.LoaderManager; @@ -35,6 +36,14 @@ protected void onCreate(Bundle savedInstanceState) { } } + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + + getSupportLoaderManager().destroyLoader(loaderId()); + initLoader(); + } + private void initLoader() { // LoaderCallbacks as an object, so no hint regarding Loader will be leak to the subclasses. getSupportLoaderManager().initLoader(loaderId(), null, new LoaderManager.LoaderCallbacks

() { diff --git a/app/src/main/java/de/xikolo/controllers/helper/VideoHelper.java b/app/src/main/java/de/xikolo/controllers/helper/VideoHelper.java index c97a66813..129199742 100644 --- a/app/src/main/java/de/xikolo/controllers/helper/VideoHelper.java +++ b/app/src/main/java/de/xikolo/controllers/helper/VideoHelper.java @@ -354,6 +354,13 @@ public int getDuration() { return (int) videoView.getDuration(); } + public boolean isPlaying() { + if (videoView != null) { + return videoView.isPlaying(); + } + return false; + } + public void play() { buttonPlay.setText(activity.getString(R.string.icon_pause)); videoView.start(); @@ -368,7 +375,7 @@ public void pause() { saveCurrentPosition(); } - private void release() { + public void release() { pause(); videoView.release(); seekBarPreviewThread.quit(); @@ -452,7 +459,6 @@ public boolean handleBackPress() { hideSettings(); return false; } - release(); return true; } @@ -530,6 +536,14 @@ public void onPlaybackSpeedClick() { public void onQualityClick() { showSettings(videoSettingsHelper.buildQualityView()); } + + @Override + public void onPipClick() { + hideSettings(); + if(controllerListener != null) { + controllerListener.onPipClick(); + } + } }, videoMode -> { if (videoMode == VideoSettingsHelper.VideoMode.HD) { @@ -722,6 +736,8 @@ public interface ControllerListener { void onSettingsOpen(); void onSettingsClosed(); + + void onPipClick(); } private static class MessageHandler extends Handler { diff --git a/app/src/main/java/de/xikolo/controllers/helper/VideoSettingsHelper.kt b/app/src/main/java/de/xikolo/controllers/helper/VideoSettingsHelper.kt index 5c24aef5f..616c231fd 100644 --- a/app/src/main/java/de/xikolo/controllers/helper/VideoSettingsHelper.kt +++ b/app/src/main/java/de/xikolo/controllers/helper/VideoSettingsHelper.kt @@ -3,7 +3,6 @@ package de.xikolo.controllers.helper import android.annotation.SuppressLint import android.content.Context import android.content.Intent -import android.os.Build import android.provider.Settings import android.support.annotation.StringRes import android.support.v4.content.ContextCompat @@ -14,6 +13,7 @@ import android.widget.LinearLayout import android.widget.TextView import de.xikolo.R import de.xikolo.config.FeatureToggle +import de.xikolo.managers.PermissionManager import de.xikolo.models.VideoSubtitles import de.xikolo.utils.PlaybackSpeedUtil import java.util.* @@ -63,6 +63,16 @@ class VideoSettingsHelper(private val context: Context, private val subtitles: L ) ) } + if(FeatureToggle.pictureInPicture(context) && PermissionManager.hasPipPermission(context)) { + list.addView( + buildSettingsItem( + R.string.icon_pip, + context.getString(R.string.video_settings_pip), + View.OnClickListener { clickListener.onPipClick() }, + false + ) + ) + } return list.parent as ViewGroup } @@ -141,12 +151,7 @@ class VideoSettingsHelper(private val context: Context, private val subtitles: L context.getString(R.string.video_settings_subtitles), context.getString(R.string.icon_settings), View.OnClickListener { - val subtitleSettings = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) - Settings.ACTION_CAPTIONING_SETTINGS - else - Settings.ACTION_ACCESSIBILITY_SETTINGS - ContextCompat.startActivity(context, Intent(subtitleSettings), null) + ContextCompat.startActivity(context, Intent(Settings.ACTION_CAPTIONING_SETTINGS), null) } ) @@ -239,6 +244,8 @@ class VideoSettingsHelper(private val context: Context, private val subtitles: L fun onPlaybackSpeedClick() fun onSubtitleClick() + + fun onPipClick() } // also invoked when old value equal to new value diff --git a/app/src/main/java/de/xikolo/controllers/section/VideoPreviewFragment.kt b/app/src/main/java/de/xikolo/controllers/section/VideoPreviewFragment.kt index 078ad8cc2..80ac911db 100644 --- a/app/src/main/java/de/xikolo/controllers/section/VideoPreviewFragment.kt +++ b/app/src/main/java/de/xikolo/controllers/section/VideoPreviewFragment.kt @@ -161,7 +161,7 @@ class VideoPreviewFragment : LoadingStatePresenterFragment @@ -214,6 +236,15 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP } } + private fun refreshPipStatus() { + val pipSettings = findPreference(getString(R.string.preference_video_pip)) + if (!PermissionManager.hasPipPermission(context)) { + pipSettings.summary = getString(R.string.settings_summary_video_pip_unavailable) + } else { + pipSettings.summary = "" + } + } + private fun buildLoginView(pref: Preference?) { if (pref != null) { pref.title = getString(R.string.login) diff --git a/app/src/main/java/de/xikolo/controllers/video/VideoActivity.java b/app/src/main/java/de/xikolo/controllers/video/VideoActivity.java index 0deff46de..e59db4d6a 100644 --- a/app/src/main/java/de/xikolo/controllers/video/VideoActivity.java +++ b/app/src/main/java/de/xikolo/controllers/video/VideoActivity.java @@ -1,8 +1,18 @@ package de.xikolo.controllers.video; +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.app.PictureInPictureParams; +import android.app.RemoteAction; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.res.Configuration; import android.graphics.Point; +import android.graphics.Rect; import android.graphics.Typeface; +import android.graphics.drawable.Icon; import android.os.Build; import android.os.Bundle; import android.support.annotation.NonNull; @@ -24,8 +34,12 @@ import com.google.android.gms.cast.framework.CastState; import com.yatatsu.autobundle.AutoBundleField; +import java.util.ArrayList; +import java.util.List; + import butterknife.BindView; import de.xikolo.R; +import de.xikolo.config.FeatureToggle; import de.xikolo.controllers.base.BasePresenterActivity; import de.xikolo.controllers.helper.VideoHelper; import de.xikolo.models.Course; @@ -42,15 +56,19 @@ import de.xikolo.utils.LanalyticsUtil; import de.xikolo.utils.MarkdownUtil; import de.xikolo.utils.PlayServicesUtil; +import de.xikolo.utils.ToastUtil; public class VideoActivity extends BasePresenterActivity implements VideoView { public static final String TAG = VideoActivity.class.getSimpleName(); + public static final String ACTION_SWITCH_PLAYBACK_STATE = "switch_playback_state"; + @AutoBundleField String courseId; @AutoBundleField String sectionId; @AutoBundleField String itemId; @AutoBundleField String videoId; + @AutoBundleField(required = false) Intent parentIntent; @BindView(R.id.videoMetadata) View videoMetadataView; @BindView(R.id.textTitle) TextView videoTitleText; @@ -64,6 +82,10 @@ public class VideoActivity extends BasePresenterActivity { @@ -138,6 +165,14 @@ public void onSettingsClosed() { updateVideoView(getResources().getConfiguration().orientation); } + @Override + protected void onNewIntent(Intent intent) { + if (intent != null) { + presenter.onPause(videoHelper.getCurrentPosition()); + super.onNewIntent(intent); + } + } + @Override public void setupVideo(Course course, Section section, Item item, Video video) { this.video = video; @@ -330,27 +365,124 @@ public boolean onOptionsItemSelected(MenuItem item) { // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); if (id == android.R.id.home) { - NavUtils.navigateUpFromSameTask(this); + navigateUp(); return true; } return super.onOptionsItemSelected(item); } + private void navigateUp() { + if (backStackLost && parentIntent != null) { + finishAndRemoveTask(); + parentIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + startActivity(parentIntent); + } else { + NavUtils.navigateUpFromSameTask(this); + } + } + + @Override + public void onUserLeaveHint() { + if (FeatureToggle.pictureInPicture(this) && videoHelper.getCurrentPosition() < videoHelper.getDuration() - 5000) { + super.onUserLeaveHint(); + enterPip(false); + } + } + @Override public void onBackPressed() { if (videoHelper.handleBackPress()) { - finish(); + navigateUp(); } } @Override - protected void onPause() { - super.onPause(); - + protected void onStop() { if (videoHelper != null) { videoHelper.pause(); presenter.onPause(videoHelper.getCurrentPosition()); } + super.onStop(); + } + + @Override + protected void onDestroy() { + if (videoHelper != null) { + videoHelper.release(); + } + super.onDestroy(); + } + + @TargetApi(26) + private void enterPip(boolean explicitInteraction) { + if (!enterPictureInPictureMode(getPipParams(videoHelper.isPlaying())) + && explicitInteraction) { + ToastUtil.show(R.string.toast_pip_error); + } + } + + @TargetApi(26) + private PictureInPictureParams getPipParams(boolean playing) { + PendingIntent pendingIntent = PendingIntent.getBroadcast( + this, + 0, + new Intent(ACTION_SWITCH_PLAYBACK_STATE), + 0); + + List actionList = new ArrayList<>(); + actionList.add( + new RemoteAction( + Icon.createWithResource( + this, + playing ? android.R.drawable.ic_media_pause : android.R.drawable.ic_media_play), + playing ? getString(R.string.video_pip_action_pause) : getString(R.string.video_pip_action_play), + playing ? getString(R.string.video_pip_action_pause) : getString(R.string.video_pip_action_play), + pendingIntent + ) + ); + + Rect pipBounds = new Rect(); + pipBounds.set( + videoHelper.getVideoContainer().getLeft(), + videoHelper.getVideoContainer().getTop(), + videoHelper.getVideoContainer().getRight(), + videoHelper.getVideoContainer().getBottom() + ); + + return new PictureInPictureParams.Builder() + .setSourceRectHint(pipBounds) + .setActions(actionList) + .build(); + } + + @Override + @TargetApi(26) + public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) { + if (isInPictureInPictureMode) { + videoHelper.hideSettings(); + videoHelper.hide(); + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null || !ACTION_SWITCH_PLAYBACK_STATE.equals(intent.getAction())) { + return; + } + + if (videoHelper.isPlaying()) { + videoHelper.pause(); + } else { + videoHelper.play(); + } + setPictureInPictureParams(getPipParams(videoHelper.isPlaying())); + } + }; + registerReceiver(broadcastReceiver, new IntentFilter(ACTION_SWITCH_PLAYBACK_STATE)); + } else { + videoHelper.show(); + unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + backStackLost = true; + } } @Override diff --git a/app/src/main/java/de/xikolo/managers/PermissionManager.java b/app/src/main/java/de/xikolo/managers/PermissionManager.java index c814d1df9..a16bacfc6 100644 --- a/app/src/main/java/de/xikolo/managers/PermissionManager.java +++ b/app/src/main/java/de/xikolo/managers/PermissionManager.java @@ -1,7 +1,10 @@ package de.xikolo.managers; import android.Manifest; +import android.annotation.TargetApi; import android.app.Activity; +import android.app.AppOpsManager; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; @@ -82,4 +85,22 @@ public static void startAppInfo(Activity activity) { activity.startActivity(intent); } + @TargetApi(26) + public static boolean hasPipPermission(Context context) { + try { + AppOpsManager manager = ContextCompat.getSystemService(context, AppOpsManager.class); + if (manager != null) { + int status = manager.checkOp( + AppOpsManager.OPSTR_PICTURE_IN_PICTURE, + context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA).uid, + context.getPackageName() + ); + if (status == AppOpsManager.MODE_ALLOWED) { + return true; + } + } + } catch (Exception ignored) { + } + return false; + } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 0bee91106..cb1ad12e3 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -215,6 +215,7 @@ Datei bereits heruntergeladen. Kein Schreibzugriff für %1$s. Keine Anwendung zum Öffnen dieser Datei gefunden. + Die Anwendung konnte nicht in den Bild-im-Bild Modus wechseln. Offline-Modus @@ -224,6 +225,7 @@ Wiedergabegeschwindigkeit Untertitel Keine + Bild-im-Bild (automatisch erzeugt) Für dieses Video ist keine Beschreibung vorhanden. @@ -250,6 +252,9 @@ Wiedergabegeschwindigkeit von Videos Standard-Geschwindigkeit: %s + Einstellungen für Bild-im-Bild + Deaktiviert oder nicht verfügbar + Info Open-Source-Lizenzen diff --git a/app/src/main/res/values/icons.xml b/app/src/main/res/values/icons.xml index baea6bbde..79ce6128d 100644 --- a/app/src/main/res/values/icons.xml +++ b/app/src/main/res/values/icons.xml @@ -34,6 +34,7 @@ + diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index 8ecdf7915..314b4cb80 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -15,6 +15,7 @@ download_network video_playback_speed video_subtitles_language + video_pip copyright imprint privacy diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d412f22cd..d8021970a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -223,6 +223,7 @@ File already downloaded. No write access for %1$s. No application to open this file found. + The application could not enter Picture-in-Picture mode. Offline Mode @@ -233,8 +234,11 @@ Subtitles None (automatically generated) + Picture-in-Picture · There is no description available for this video. + Play + Pause CAST @@ -257,6 +261,9 @@ Video playback speed Default speed: %s + Settings for Picture-in-Picture + Deactivated or not available + About Open source licenses diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index d418f4e9d..a8863d6ba 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -47,6 +47,10 @@ android:summary="@string/settings_summary_video_playback_speed" android:title="@string/settings_title_video_playback_speed" /> + +