diff --git a/android-versions.toml b/android-versions.toml index cc17e5fbf..a45d5858a 100644 --- a/android-versions.toml +++ b/android-versions.toml @@ -5,5 +5,5 @@ # Since properties strings are not quoted, we unquote the values in Gradle. cmdLineToolsVersion = "16.0-rc01" platformToolsVersion = "35.0.2" -buildToolsVersions = "35.0.0-rc4" -platformVersions = "35" +buildToolsVersions = "36.0.0" +platformVersions = "36" diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2e38920eb..def6fee51 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -45,12 +45,11 @@ android { compileSdk = versions.getProperty("platformVersions").toInt() applicationId = "be.ugent.zeus.hydra" minSdk = 21 - targetSdk = 35 + targetSdk = 36 versionCode = 37400 versionName = "3.7.4" vectorDrawables.useSupportLibrary = true testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - resourceConfigurations += listOf("nl", "en") // For a description of what these do, see the config.properties file. buildConfigField("boolean", "DEBUG_HOME_STREAM_PRIORITY", props.getProperty("hydra.debug.home.stream.priority")) @@ -67,6 +66,12 @@ android { } } } + + androidResources { + // Typical for Android, the old way is deprecated and the new way not stable yet. + @Suppress("UnstableApiUsage") + localeFilters += listOf("nl", "en") + } if (props.getProperty("signing").toBoolean()) { @@ -167,7 +172,7 @@ android { lint { disable += listOf( - "RtlSymmetry", "VectorPath", "Overdraw", "GradleDependency", "NotificationPermission", "OldTargetApi", "AndroidGradlePluginVersion" + "RtlSymmetry", "VectorPath", "Overdraw", "GradleDependency", "NotificationPermission", "AndroidGradlePluginVersion" ) showAll = true warningsAsErrors = true @@ -198,10 +203,8 @@ dependencies { implementation(libs.picasso) implementation(libs.cachapa) implementation(project(":material-intro")) + implementation(project(":ipcam-view")) implementation(libs.once) - implementation(libs.materialvalues) - implementation(libs.insetter) - implementation(libs.ipcam) annotationProcessor(libs.recordbuilder.processor) compileOnly(libs.recordbuilder.core) diff --git a/app/src/main/java/be/ugent/zeus/hydra/MainActivity.java b/app/src/main/java/be/ugent/zeus/hydra/MainActivity.java index 9093885d5..f0cf6dc2d 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/MainActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/MainActivity.java @@ -65,7 +65,7 @@ import com.google.android.material.navigation.NavigationView; import com.google.android.material.shape.MaterialShapeDrawable; import com.google.android.material.tabs.TabLayout; -import dev.chrisbanes.insetter.Insetter; +import androidx.core.view.ViewCompat; import jonathanfinerty.once.Once; import org.jetbrains.annotations.NotNull; @@ -233,9 +233,12 @@ protected void onCreate(Bundle savedInstanceState) { // Prevent navigation drawer from extending behind the notification bar. // It doesn't work to just set fitsSystemWindows=true, since this adds the padding // for the navigation bar to the header, which is not what you want. - Insetter.builder() - .padding(WindowInsetsCompat.Type.statusBars()) - .applyToView(this.binding.navigationView.getHeaderView(0)); + ViewCompat.setOnApplyWindowInsetsListener(this.binding.navigationView.getHeaderView(0), (v, insets) -> { + androidx.core.graphics.Insets statusInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars()); + v.setPadding(statusInsets.left, statusInsets.top, statusInsets.right, statusInsets.bottom); + return insets; + }); + ViewCompat.requestApplyInsets(this.binding.navigationView.getHeaderView(0)); if (savedInstanceState != null) { isOnboardingOpen = savedInstanceState.getBoolean(STATE_IS_ONBOARDING_OPEN, false); diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/ui/WebViewActivity.java b/app/src/main/java/be/ugent/zeus/hydra/common/ui/WebViewActivity.java index cb5584d98..0cef1e709 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/common/ui/WebViewActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/common/ui/WebViewActivity.java @@ -29,6 +29,8 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.ProgressBar; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import be.ugent.zeus.hydra.databinding.ActivityWebviewBinding; @@ -47,6 +49,12 @@ public class WebViewActivity extends BaseActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(ActivityWebviewBinding::inflate); + ViewCompat.setOnApplyWindowInsetsListener(binding.webContainer, (v, insets) -> { + int bottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom; + v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), bottom); + return insets; + }); + ViewCompat.requestApplyInsets(binding.webContainer); binding.webView.getSettings().setJavaScriptEnabled(true); binding.webView.setWebViewClient(new ProgressClient(binding.progressBar.progressBar)); diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/ui/widgets/MenuTable.java b/app/src/main/java/be/ugent/zeus/hydra/common/ui/widgets/MenuTable.java index a720adc50..e8d8c0eb7 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/common/ui/widgets/MenuTable.java +++ b/app/src/main/java/be/ugent/zeus/hydra/common/ui/widgets/MenuTable.java @@ -26,6 +26,7 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; +import android.util.TypedValue; import android.widget.TableLayout; import android.widget.TableRow; import android.widget.TextView; @@ -103,7 +104,11 @@ private void init(Context context, @Nullable AttributeSet attrs) { */ private void createText(String text, boolean isTitle, boolean isHtml) { - final int rowPadding = getContext().getResources().getDimensionPixelSize(R.dimen.material_baseline_grid_1x); + final int rowPadding = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 8f, + getContext().getResources().getDisplayMetrics() + ); TableRow tr = new TableRow(getContext()); TableRow.LayoutParams lp = new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT); diff --git a/app/src/main/java/be/ugent/zeus/hydra/feed/cards/library/LibraryViewHolder.java b/app/src/main/java/be/ugent/zeus/hydra/feed/cards/library/LibraryViewHolder.java index e398ef5f2..40601dc7b 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/feed/cards/library/LibraryViewHolder.java +++ b/app/src/main/java/be/ugent/zeus/hydra/feed/cards/library/LibraryViewHolder.java @@ -25,6 +25,7 @@ import android.content.Context; import android.content.Intent; import android.util.Pair; +import android.util.TypedValue; import android.view.View; import android.widget.LinearLayout; import android.widget.TableRow; @@ -62,7 +63,11 @@ public LibraryViewHolder(View itemView, HomeFeedAdapter adapter) { Context c = itemView.getContext(); styleName = ViewUtils.getAttr(c, R.attr.textAppearanceBodyMedium); styleHours = ViewUtils.getAttr(c, R.attr.textAppearanceCaption); - rowPadding = c.getResources().getDimensionPixelSize(R.dimen.material_baseline_grid_1x); + rowPadding = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 8f, + c.getResources().getDisplayMetrics() + ); itemView.setOnClickListener(v -> { Intent intent = new Intent(v.getContext(), MainActivity.class); diff --git a/app/src/main/java/be/ugent/zeus/hydra/resto/salad/SaladActivity.java b/app/src/main/java/be/ugent/zeus/hydra/resto/salad/SaladActivity.java index cda06802d..add8d699b 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/resto/salad/SaladActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/resto/salad/SaladActivity.java @@ -26,6 +26,8 @@ import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import be.ugent.zeus.hydra.R; @@ -54,6 +56,12 @@ public class SaladActivity extends BaseActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(ActivityRestoSaladBinding::inflate); + ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView, (v, insets) -> { + int bottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom; + v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), bottom); + return insets; + }); + ViewCompat.requestApplyInsets(binding.recyclerView); final ViewModelProvider provider = new ViewModelProvider(this); viewModel = provider.get(SaladViewModel.class); diff --git a/app/src/main/java/be/ugent/zeus/hydra/urgent/player/BecomingNoisyReceiver.java b/app/src/main/java/be/ugent/zeus/hydra/urgent/player/BecomingNoisyReceiver.java index 00b91b712..522da1af0 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/urgent/player/BecomingNoisyReceiver.java +++ b/app/src/main/java/be/ugent/zeus/hydra/urgent/player/BecomingNoisyReceiver.java @@ -26,6 +26,7 @@ import android.media.AudioManager; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; +import androidx.core.content.ContextCompat; /** * Helper class for listening for when headphones are unplugged (or the audio @@ -46,7 +47,7 @@ class BecomingNoisyReceiver extends BroadcastReceiver { void register() { if (!registered) { - context.registerReceiver(this, noisyIntentFilter); + ContextCompat.registerReceiver(context, this, noisyIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED); registered = true; } } diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/account/ApiKeyManagementActivity.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/account/ApiKeyManagementActivity.java index ecb064c96..d86993cd5 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/wpi/account/ApiKeyManagementActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/account/ApiKeyManagementActivity.java @@ -27,6 +27,8 @@ import android.text.Editable; import android.text.TextWatcher; import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import java.util.Objects; @@ -47,7 +49,13 @@ public class ApiKeyManagementActivity extends BaseActivity { + int bottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom; + v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), bottom); + return insets; + }); + ViewCompat.requestApplyInsets(binding.scroll); + binding.apiTab.setText(AccountManager.getTabKey(this)); binding.apiTap.setText(AccountManager.getTapKey(this)); binding.apiUsername.setText(AccountManager.getUsername(this)); diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/CammieActivity.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/CammieActivity.java index d4e6fbd5c..af607d999 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/CammieActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/CammieActivity.java @@ -38,7 +38,6 @@ import be.ugent.zeus.hydra.common.network.NetworkState; import be.ugent.zeus.hydra.common.ui.BaseActivity; import be.ugent.zeus.hydra.databinding.ActivityWpiCammieBinding; -import com.github.niqdev.mjpeg.DisplayMode; import com.github.niqdev.mjpeg.Mjpeg; /** @@ -118,7 +117,6 @@ private void loadMjpeg() { .open(Endpoints.CAMMIE, 5) .subscribe(inputStream -> { binding.cammieViewer.setSource(inputStream); - binding.cammieViewer.setDisplayMode(DisplayMode.BEST_FIT); binding.cammieViewer.showFps(true); }); } diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/FullScreenCammieActivity.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/FullScreenCammieActivity.java index 51931c433..6f6ef87ef 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/FullScreenCammieActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/FullScreenCammieActivity.java @@ -31,7 +31,6 @@ import be.ugent.zeus.hydra.common.network.Endpoints; import be.ugent.zeus.hydra.common.ui.BaseActivity; import be.ugent.zeus.hydra.databinding.ActivityWpiFullScreenCammieBinding; -import com.github.niqdev.mjpeg.DisplayMode; import com.github.niqdev.mjpeg.Mjpeg; /** @@ -94,7 +93,6 @@ private void loadMjpeg() { .open(Endpoints.CAMMIE, 5) .subscribe(inputStream -> { binding.cammieViewer.setSource(inputStream); - binding.cammieViewer.setDisplayMode(DisplayMode.BEST_FIT); binding.cammieViewer.showFps(true); }); } diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/FormActivity.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/FormActivity.java index 05f6cdab3..a9a5e1485 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/FormActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/FormActivity.java @@ -30,6 +30,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.BundleCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import java.math.BigDecimal; @@ -65,6 +67,12 @@ public class FormActivity extends BaseActivity { + int bottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom; + v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), bottom); + return insets; + }); + ViewCompat.requestApplyInsets(binding.scroll); if (savedInstanceState != null && savedInstanceState.containsKey(KEY_FORM_OBJECT)) { formObject = BundleCompat.getParcelable(savedInstanceState, KEY_FORM_OBJECT, TransactionForm.class); diff --git a/app/src/main/res/layout-land/activity_wpi_cammie.xml b/app/src/main/res/layout-land/activity_wpi_cammie.xml index 040a8e0e4..58b3b4a93 100644 --- a/app/src/main/res/layout-land/activity_wpi_cammie.xml +++ b/app/src/main/res/layout-land/activity_wpi_cammie.xml @@ -53,8 +53,7 @@ app:layout_constraintDimensionRatio="H,20:11" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintWidth_percent="0.5" - stream:type="stream_default" /> + app:layout_constraintWidth_percent="0.5" /> diff --git a/app/src/main/res/layout/activity_resto_salad.xml b/app/src/main/res/layout/activity_resto_salad.xml index 380a33aee..c4496c06b 100644 --- a/app/src/main/res/layout/activity_resto_salad.xml +++ b/app/src/main/res/layout/activity_resto_salad.xml @@ -27,6 +27,7 @@ android:id="@+id/coordinator_layout" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".resto.salad.SaladActivity"> diff --git a/app/src/main/res/layout/activity_webview.xml b/app/src/main/res/layout/activity_webview.xml index 1f841275c..2817a479a 100644 --- a/app/src/main/res/layout/activity_webview.xml +++ b/app/src/main/res/layout/activity_webview.xml @@ -26,6 +26,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".common.ui.WebViewActivity"> diff --git a/app/src/main/res/layout/activity_wpi_cammie.xml b/app/src/main/res/layout/activity_wpi_cammie.xml index 909218b25..ff48f27c6 100644 --- a/app/src/main/res/layout/activity_wpi_cammie.xml +++ b/app/src/main/res/layout/activity_wpi_cammie.xml @@ -22,12 +22,12 @@ + tools:context=".wpi.cammie.CammieActivity" + android:fitsSystemWindows="true"> + app:layout_constraintTop_toTopOf="parent" /> + android:layout_height="match_parent" /> diff --git a/app/src/main/res/layout/activity_wpi_tap_cart.xml b/app/src/main/res/layout/activity_wpi_tap_cart.xml index 3dd607889..420eee139 100644 --- a/app/src/main/res/layout/activity_wpi_tap_cart.xml +++ b/app/src/main/res/layout/activity_wpi_tap_cart.xml @@ -27,6 +27,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".wpi.tab.create.FormActivity" + android:fitsSystemWindows="true" tools:menu="@menu/menu_resto"> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 78d3f66c3..05dc0ef36 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -83,14 +83,14 @@ #095BBF - @color/material_color_white + #FFFFFF - @color/material_color_grey_100 + #F5F5F5 @color/md_theme_light_primary @color/wpi_theme_light_primary diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index e6162d93b..cafbd1d70 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -84,4 +84,24 @@ 64dp + + + 8dp + 6dp + 56dp + 40dp + 8dp + 16dp + 20sp + 56dp + 16dp + 24dp + 16dp + 24dp + 24dp + 16dp + 12dp + 16sp + + diff --git a/flake.nix b/flake.nix index 818df4afb..4d88ce462 100644 --- a/flake.nix +++ b/flake.nix @@ -26,7 +26,7 @@ platformToolsVersion = androidVersions.platformToolsVersion; buildToolsVersions = [ androidVersions.buildToolsVersions ]; includeEmulator = true; - emulatorVersion = "35.2.5"; + emulatorVersion = "36.3.10"; includeSystemImages = true; systemImageTypes = [ "google_apis_playstore" ]; abiVersions = [ "x86_64" ]; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a84ce1e1f..e26abe495 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,10 +2,11 @@ androidxTest = '1.7.0' espresso = '3.7.0' livedata = '2.9.2' +mjpeg = "3.0.0" okhttp3 = '4.12.0' recordbuilder = '52' room = '2.6.1' -buildtool = "8.7.3" +buildtool = "8.12.0" [libraries] android-build-tool = { module = 'com.android.tools.build:gradle', version.ref = 'buildtool' } @@ -41,14 +42,11 @@ equalsverifier = { module = 'nl.jqno.equalsverifier:equalsverifier', version = ' firebase-analytics = { module = 'com.google.firebase:firebase-analytics', version = '23.0.0' } firebase-crashlytics = { module = 'com.google.firebase:firebase-crashlytics', version = '19.4.1' } guava = { module = 'com.google.guava:guava', version = '33.4.8-jre' } -insetter = { module = 'dev.chrisbanes.insetter:insetter', version = '0.6.1' } -ipcam = { module = 'com.github.niqdev:ipcam-view', version = '2.4.1' } jetbrains-annotations = { module = 'org.jetbrains:annotations', version = '26.1.0' } jsonassert = { module = 'org.skyscreamer:jsonassert', version = '1.5.3' } junit = { module = 'junit:junit', version = '4.13.2' } leakcanary = { module = 'com.squareup.leakcanary:leakcanary-android', version = '2.14' } material = { module = 'com.google.android.material:material', version = '1.13.0' } -materialvalues = { module = 'com.github.esnaultdev:MaterialValues', version = 'v1.1.1' } mockito-core = { module = 'org.mockito:mockito-core', version = '5.22.0' } mockito-inline = { module = 'org.mockito:mockito-inline', version = '5.2.0' } moshi = { module = 'com.squareup.moshi:moshi', version = '1.15.2' } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 79eb9d003..ed4c299ad 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/ipcam-view/README.md b/ipcam-view/README.md new file mode 100644 index 000000000..48e3dd20a --- /dev/null +++ b/ipcam-view/README.md @@ -0,0 +1,137 @@ +Vendored version from ipcam view 3.0.0. + +We just include the `mjpeg-view` folder. + + +# ipcam-view ![ipcam-view](images/logo.png) + +[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-ipcam--view-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/3358) +[![JitPack](https://jitpack.io/v/niqdev/ipcam-view.svg)](https://jitpack.io/#niqdev/ipcam-view) + +Android MJPEG video streaming made simple! + +A wrapper library around the well known [SimpleMjpegView](https://bitbucket.org/neuralassembly/simplemjpegview) and [android-camera-axis](https://code.google.com/archive/p/android-camera-axis/) projects. + +If you have problem to identify your IpCam url, please follow this [link](https://github.com/niqdev/ipcam-view/wiki) + +### Features +- [x] Default support by `android-camera-axis` +- [ ] Native support by `SimpleMjpegView` +- [x] Handle credentials and cookies +- [x] Multiple camera in one activity +- [x] Snapshot +- [x] Flip and rotate image +- [x] Video recording +- [x] Custom appearance + +### Gradle dependency +```java +repositories { + maven { url 'https://jitpack.io' } +} +dependencies { + implementation 'com.github.niqdev:ipcam-view:' +} +``` + +### Demo app + +main default + +two-camera snapshot + +video custom-appearance + +settings + +[](https://f-droid.org/packages/com.github.niqdev.ipcam/) +[](https://play.google.com/store/apps/details?id=com.github.niqdev.ipcam) + +### Usage + +Add to your layout: [example](app/src/main/res/layout/activity_ipcam_default.xml) +```java + + + + + + +``` + +Read stream in your activity/fragment: [example](app/src/main/java/com/github/niqdev/ipcam/IpCamDefaultActivity.kt) +```java +int TIMEOUT = 5; //seconds + +Mjpeg.newInstance() + .credential("USERNAME", "PASSWORD") + .open("IPCAM_URL.mjpg", TIMEOUT) + .subscribe(inputStream -> { + mjpegView.setSource(inputStream); + mjpegView.setDisplayMode(DisplayMode.BEST_FIT); + mjpegView.showFps(true); + }); +``` + +### Customize appearance + +To get a transparent background for the surface itself (while stream is loading) as well as for the stream background +```java +mjpegView.setTransparentBackground(); +// OR +stream:transparentBackground="true" +``` + +To hide the MjpegView later, you might need to reset the transparency due to internal behaviour of applying transparency +```java +mjpegView.resetTransparentBackground(); +``` + +To set other colors than transparent, be aware that they will only be applied on a running stream i.e. you can't change the color of the surface itself which you will see while the stream is loading + +Note that it only works when `transparentBackground` is not set to `true` and that you are not able to directly set transparent background color here +```java +mjpegView.setCustomBackgroundColor(Color.DKGRAY); +// OR +stream:backgroundColor="@android:color/darker_gray" +``` + +To change the colors of the fps overlay +```java +mjpegView.setFpsOverlayBackgroundColor(Color.DKGRAY); +mjpegView.setFpsOverlayTextColor(Color.WHITE); +``` + +To clear the last frame since the canvas keeps the current image even if you stop the stream, e.g. hide/show +```java +mjpegView.clearStream(); +``` + +To flip the image +```java +mjpegView.flipHorizontal(true); +mjpegView.flipVertical(true); +``` + +To rotate the image +```java +mjpegView.setRotate(90); // degrees +``` + +### Apps that use this library +* [OpenWebNet Android](https://github.com/openwebnet/openwebnet-android) +* [TankDroid](https://github.com/bmachek/TankDroid) + +You are welcome to add your app to the list! diff --git a/ipcam-view/build.gradle.kts b/ipcam-view/build.gradle.kts new file mode 100644 index 000000000..745b594e8 --- /dev/null +++ b/ipcam-view/build.gradle.kts @@ -0,0 +1,77 @@ +import java.io.FileInputStream +import java.util.* + +plugins { + id(libs.plugins.android.build.library.get().pluginId) + id("org.jetbrains.kotlin.android") version "2.3.10" +} + +val versions = loadAndroidVersions() + +android { + namespace = "com.github.niqdev.mjpeg" + + // We need this for Nix flakes + buildToolsVersion = versions.getProperty("buildToolsVersions") + + defaultConfig { + compileSdk = versions.getProperty("platformVersions").toInt() + minSdk = 21 + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") + } + } + lint { + disable += listOf("Overdraw", "OldTargetApi", "GradleDependency") + showAll = true + warningsAsErrors = true + targetSdk = versions.getProperty("platformVersions").toInt() + } + + packaging { + resources { + excludes.add("META-INF/services/javax.annotation.processing.Processor") + } + } + + useLibrary("org.apache.http.legacy") + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } +} + +dependencies { + implementation(libs.androidx.appcompat) + api("io.reactivex:rxjava:1.3.8") // it"s obsolete + api("io.reactivex:rxandroid:1.2.1") + implementation("androidx.core:core-ktx:1.17.0") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.22") +} + +// TODO: extract this duplicate code... +/** + * Loads the default properties, and the user properties. This will also load the + * secret keys. + */ +fun loadAndroidVersions(): Properties { + val defaultProps = Properties() + defaultProps.load(FileInputStream(file("../android-versions.toml"))) + + val strippedProps = Properties() + defaultProps.forEach { k, v -> + strippedProps.setProperty(k.toString(), v.toString().replace("\"", "")) + } + + return strippedProps +} diff --git a/ipcam-view/proguard-rules.pro b/ipcam-view/proguard-rules.pro new file mode 100644 index 000000000..06d16ae7a --- /dev/null +++ b/ipcam-view/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/niqdev/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/ipcam-view/src/main/AndroidManifest.xml b/ipcam-view/src/main/AndroidManifest.xml new file mode 100644 index 000000000..cc947c567 --- /dev/null +++ b/ipcam-view/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/AbstractMjpegView.kt b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/AbstractMjpegView.kt new file mode 100644 index 000000000..ecb2ce634 --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/AbstractMjpegView.kt @@ -0,0 +1,20 @@ +package com.github.niqdev.mjpeg + +import android.view.SurfaceHolder + +abstract class AbstractMjpegView : MjpegView { + abstract fun onSurfaceCreated(holder: SurfaceHolder) + abstract fun onSurfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) + abstract fun onSurfaceDestroyed(holder: SurfaceHolder) + + companion object { + protected const val POSITION_UPPER_LEFT = 9 + protected const val POSITION_UPPER_RIGHT = 3 + protected const val POSITION_LOWER_LEFT = 12 + protected const val POSITION_LOWER_RIGHT = 6 + protected const val SIZE_STANDARD = 1 + protected const val SIZE_BEST_FIT = 4 + protected const val SIZE_SCALE_FIT = 16 + protected const val SIZE_FULLSCREEN = 8 + } +} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/Mjpeg.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/Mjpeg.java new file mode 100644 index 000000000..579322b01 --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/Mjpeg.java @@ -0,0 +1,79 @@ +package com.github.niqdev.mjpeg; + +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +import androidx.annotation.NonNull; +import rx.Observable; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; + +/** + * A library wrapper for handle mjpeg streams. + * + *
  • simplemjpegview
  • + *
  • android-camera-axis
  • + * + */ +public class Mjpeg { + private static final String TAG = Mjpeg.class.getSimpleName(); + + public static Mjpeg newInstance() { + return new Mjpeg(); + } + + @NonNull + private Observable connect(String url) { + return Observable.defer(() -> { + try { + HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); + loadConnectionProperties(urlConnection); + InputStream inputStream = urlConnection.getInputStream(); + return Observable.just(new MjpegInputStream(inputStream)); + } catch (IOException e) { + Log.e(TAG, "error during connection", e); + return Observable.error(e); + } + }); + } + + /** + * Connect to a Mjpeg stream. + * + * @param url source + * @return Observable Mjpeg stream + */ + public Observable open(String url) { + return connect(url) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + /** + * Connect to a Mjpeg stream. + * + * @param url source + * @param timeout in seconds + * @return Observable Mjpeg stream + */ + public Observable open(String url, int timeout) { + return connect(url) + .timeout(timeout, TimeUnit.SECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + /** + * Configure request properties + * + * @param urlConnection the url connection to add properties and cookies to + */ + private void loadConnectionProperties(HttpURLConnection urlConnection) { + urlConnection.setRequestProperty("Cache-Control", "no-cache"); + } +} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStream.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStream.java new file mode 100644 index 000000000..91b2d8f38 --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStream.java @@ -0,0 +1,97 @@ +package com.github.niqdev.mjpeg; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import java.io.*; +import java.util.Properties; + +/* + * I don't really understand and want to know what the hell it does! + * Maybe one day I will refactor it ;-) + *

    + * https://code.google.com/archive/p/android-camera-axis + */ +public class MjpegInputStream extends DataInputStream { + private final static int HEADER_MAX_LENGTH = 100; + private final static int FRAME_MAX_LENGTH = 200000 + HEADER_MAX_LENGTH; + private final byte[] SOI_MARKER = {(byte) 0xFF, (byte) 0xD8}; + private final byte[] EOF_MARKER = {(byte) 0xFF, (byte) 0xD9}; + private int mContentLength = -1; + + MjpegInputStream(InputStream in) { + super(new BufferedInputStream(in, FRAME_MAX_LENGTH)); + } + + private int getEndOfSequence(DataInputStream in, byte[] sequence) throws IOException { + int seqIndex = 0; + byte c; + for (int i = 0; i < FRAME_MAX_LENGTH; i++) { + c = (byte) in.readUnsignedByte(); + if (c == sequence[seqIndex]) { + seqIndex++; + if (seqIndex == sequence.length) { + return i + 1; + } + } else { + seqIndex = 0; + } + } + return -1; + } + + private int getStartOfSequence(DataInputStream in, byte[] sequence) throws IOException { + int end = getEndOfSequence(in, sequence); + return (end < 0) ? (-1) : (end - sequence.length); + } + + private int parseContentLength(byte[] headerBytes) throws IOException, IllegalArgumentException { + ByteArrayInputStream headerIn = new ByteArrayInputStream(headerBytes); + Properties props = new Properties(); + props.load(headerIn); + return Integer.parseInt(props.getProperty("Content-Length")); + } + + byte[] readHeader() throws IOException { + mark(FRAME_MAX_LENGTH); + int headerLen = getStartOfSequence(this, SOI_MARKER); + reset(); + byte[] header = new byte[headerLen]; + readFully(header); + return header; + } + + // no more accessible + byte[] readMjpegFrame(byte[] header) throws IOException { + try { + mContentLength = parseContentLength(header); + } catch (IllegalArgumentException iae) { + mContentLength = getEndOfSequence(this, EOF_MARKER); + } + reset(); + byte[] frameData = new byte[mContentLength]; + skipBytes(header.length); + readFully(frameData); + return frameData; + } + + // no more accessible + Bitmap readMjpegFrame() throws IOException { + mark(FRAME_MAX_LENGTH); + int headerLen = getStartOfSequence(this, SOI_MARKER); + reset(); + byte[] header = new byte[headerLen]; + readFully(header); + try { + mContentLength = parseContentLength(header); + } catch (IllegalArgumentException iae) { + mContentLength = getEndOfSequence(this, EOF_MARKER); + } + reset(); + byte[] frameData = new byte[mContentLength]; + skipBytes(headerLen); + readFully(frameData); + return BitmapFactory.decodeStream(new ByteArrayInputStream(frameData)); + } + +} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegSurfaceView.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegSurfaceView.java new file mode 100644 index 000000000..18de92834 --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegSurfaceView.java @@ -0,0 +1,79 @@ +package com.github.niqdev.mjpeg; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import androidx.annotation.NonNull; + +public class MjpegSurfaceView extends SurfaceView implements SurfaceHolder.Callback, MjpegView { + + private final AbstractMjpegView mMjpegView; + + public MjpegSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + mMjpegView = new MjpegViewDefault(this, this); + } + + @Override + public void surfaceCreated(@NonNull SurfaceHolder holder) { + mMjpegView.onSurfaceCreated(holder); + } + + @Override + public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { + mMjpegView.onSurfaceChanged(holder, format, width, height); + } + + @Override + public void surfaceDestroyed(@NonNull SurfaceHolder holder) { + mMjpegView.onSurfaceDestroyed(holder); + } + + @Override + public void setSource(@NonNull MjpegInputStream stream) { + mMjpegView.setSource(stream); + } + + @Override + public void showFps(boolean show) { + mMjpegView.showFps(show); + } + + @Override + public void flipSource(boolean flip) { + mMjpegView.flipSource(flip); + } + + @Override + public void flipHorizontal(boolean flip) { + mMjpegView.flipHorizontal(flip); + } + + @Override + public void flipVertical(boolean flip) { + mMjpegView.flipVertical(flip); + } + + @Override + public void setRotate(float degrees) { + mMjpegView.setRotate(degrees); + } + + @Override + public void stopPlayback() { + mMjpegView.stopPlayback(); + } + + @Override + public boolean isStreaming() { + return mMjpegView.isStreaming(); + } + + @NonNull + @Override + public SurfaceView getSurfaceView() { + return this; + } +} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegView.kt b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegView.kt new file mode 100644 index 000000000..420bbba9a --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegView.kt @@ -0,0 +1,15 @@ +package com.github.niqdev.mjpeg + +import android.view.SurfaceView + +interface MjpegView { + fun setSource(stream: MjpegInputStream) + fun showFps(show: Boolean) + fun flipSource(flip: Boolean) + fun flipHorizontal(flip: Boolean) + fun flipVertical(flip: Boolean) + fun setRotate(degrees: Float) + fun stopPlayback() + val isStreaming: Boolean + val surfaceView: SurfaceView +} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewDefault.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewDefault.java new file mode 100644 index 000000000..cb08430fc --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewDefault.java @@ -0,0 +1,307 @@ +package com.github.niqdev.mjpeg; + +import android.graphics.*; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import androidx.annotation.NonNull; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +/* + * I don't really understand and want to know what the hell it does! + * Maybe one day I will refactor it ;-) + *

    + * https://code.google.com/archive/p/android-camera-axis + */ +public class MjpegViewDefault extends AbstractMjpegView { + private static final String TAG = MjpegViewDefault.class.getSimpleName(); + + private final SurfaceHolder.Callback mSurfaceHolderCallback; + private final SurfaceView mSurfaceView; + private MjpegViewThread thread; + private MjpegInputStream mIn = null; + private boolean showFps = false; + private boolean flipHorizontal = false; + private boolean flipVertical = false; + private float rotateDegrees = 0; + private volatile boolean mRun = false; + private volatile boolean surfaceDone = false; + private Paint overlayPaint; + private int ovlPos; + private int dispWidth; + private int dispHeight; + private boolean resume = false; + + MjpegViewDefault(SurfaceView surfaceView, SurfaceHolder.Callback callback) { + this.mSurfaceView = surfaceView; + this.mSurfaceHolderCallback = callback; + init(); + } + + Bitmap flip(Bitmap src) { + Matrix m = new Matrix(); + float sx = flipHorizontal ? -1 : 1; + float sy = flipVertical ? -1 : 1; + m.preScale(sx, sy); + Bitmap dst = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), m, false); + dst.setDensity(DisplayMetrics.DENSITY_DEFAULT); + return dst; + } + + Bitmap rotate(Bitmap src, float degrees) { + Matrix m = new Matrix(); + m.setRotate(degrees); + return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), m, false); + } + + private void init() { + + SurfaceHolder holder = mSurfaceView.getHolder(); + holder.addCallback(mSurfaceHolderCallback); + thread = new MjpegViewThread(holder); + mSurfaceView.setFocusable(true); + if (!resume) { + resume = true; + overlayPaint = new Paint(); + overlayPaint.setTextAlign(Paint.Align.LEFT); + overlayPaint.setTextSize(12); + overlayPaint.setTypeface(Typeface.DEFAULT); + ovlPos = MjpegViewDefault.POSITION_LOWER_RIGHT; + dispWidth = mSurfaceView.getWidth(); + dispHeight = mSurfaceView.getHeight(); + } + } + + /* all methods/constructors below are no more accessible */ + + void _startPlayback() { + if (mIn != null && thread != null) { + mRun = true; + /* + * clear canvas cache + * @see https://github.com/niqdev/ipcam-view/issues/14 + */ + //noinspection deprecation + mSurfaceView.destroyDrawingCache(); + thread.start(); + } + } + + void _resumePlayback() { + mRun = true; + init(); + thread.start(); + } + + @Override + public void onSurfaceCreated(@NonNull SurfaceHolder holder) { + surfaceDone = true; + } + + @Override + public void onSurfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { + if (thread != null) { + thread.setSurfaceSize(width, height); + } + } + + @Override + public void onSurfaceDestroyed(@NonNull SurfaceHolder holder) { + surfaceDone = false; + stopPlayback(); + if (thread != null) { + thread = null; + } + } + + @Override + public void setSource(@NonNull MjpegInputStream stream) { + // see https://github.com/niqdev/ipcam-view/issues/14 + mIn = stream; + // make sure resume is calling _resumePlayback() + if (!resume) { + _startPlayback(); + } else { + _resumePlayback(); + } + } + + @Override + public void showFps(boolean show) { + showFps = show; + } + + @Override + public void flipSource(boolean flip) { + flipHorizontal(flip); + } + + @Override + public void flipHorizontal(boolean flip) { + this.flipHorizontal = flip; + } + + @Override + public void flipVertical(boolean flip) { + this.flipVertical = flip; + } + + @Override + public void setRotate(float degrees) { + rotateDegrees = degrees; + } + + @Override + public synchronized void stopPlayback() { + mRun = false; + boolean retry = true; + while (retry) { + try { + // make sure the thread is not null + if (thread != null) { + thread.join(500); + } + retry = false; + } catch (InterruptedException e) { + Log.e(TAG, "error stopping playback thread", e); + } + } + + // close the connection + if (mIn != null) { + try { + mIn.close(); + } catch (IOException e) { + Log.e(TAG, "error closing input stream", e); + } + mIn = null; + } + } + + @Override + public boolean isStreaming() { + return mRun; + } + + @NonNull + @Override + public SurfaceView getSurfaceView() { + return mSurfaceView; + } + + class MjpegViewThread extends Thread { + private final SurfaceHolder mSurfaceHolder; + private int frameCounter = 0; + private Bitmap ovl; + + MjpegViewThread(SurfaceHolder surfaceHolder) { + mSurfaceHolder = surfaceHolder; + } + + private Rect destRect(int bmw, int bmh) { + float bmasp = (float) bmw / (float) bmh; + bmw = dispWidth; + bmh = (int) (dispWidth / bmasp); + if (bmh > dispHeight) { + bmh = dispHeight; + bmw = (int) (dispHeight * bmasp); + } + int tempx = (dispWidth / 2) - (bmw / 2); + int tempy = (dispHeight / 2) - (bmh / 2); + return new Rect(tempx, tempy, bmw + tempx, bmh + tempy); + } + + void setSurfaceSize(int width, int height) { + synchronized (mSurfaceHolder) { + dispWidth = width; + dispHeight = height; + } + } + + private Bitmap makeFpsOverlay(Paint p, String text) { + Rect b = new Rect(); + p.getTextBounds(text, 0, text.length(), b); + int bwidth = b.width() + 2; + int bheight = b.height() + 2; + Bitmap bm = Bitmap.createBitmap(bwidth, bheight, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(bm); + p.setColor(Color.BLACK); + c.drawRect(0, 0, bwidth, bheight, p); + p.setColor(Color.WHITE); + c.drawText(text, -b.left + 1, (bheight / 2) - ((p.ascent() + p.descent()) / 2) + 1, p); + return bm; + } + + public void run() { + long start = System.currentTimeMillis(); + PorterDuffXfermode mode = new PorterDuffXfermode( + PorterDuff.Mode.DST_OVER); + Bitmap bm; + int width; + int height; + Rect destRect; + Canvas c = null; + Paint p = new Paint(); + String fps; + while (mRun) { + if (surfaceDone) { + try { + c = mSurfaceHolder.lockCanvas(); + + if (c == null) { + Log.w(TAG, "null canvas, skipping render"); + continue; + } + synchronized (mSurfaceHolder) { + try { + byte[] header = mIn.readHeader(); + byte[] imageData = mIn.readMjpegFrame(header); + bm = BitmapFactory.decodeStream(new ByteArrayInputStream(imageData)); + if (flipHorizontal || flipVertical) { + bm = flip(bm); + } + if (rotateDegrees != 0) { + bm = rotate(bm, rotateDegrees); + } + + destRect = destRect(bm.getWidth(), bm.getHeight()); + + c.drawColor(Color.BLACK); + + c.drawBitmap(bm, null, destRect, p); + + if (showFps) { + p.setXfermode(mode); + if (ovl != null) { + height = ((ovlPos & 1) == 1) ? destRect.top : destRect.bottom - ovl.getHeight(); + width = ((ovlPos & 8) == 8) ? destRect.left : destRect.right - ovl.getWidth(); + c.drawBitmap(ovl, width, height, null); + } + p.setXfermode(null); + frameCounter++; + if ((System.currentTimeMillis() - start) >= 1000) { + fps = frameCounter + "fps"; + frameCounter = 0; + start = System.currentTimeMillis(); + ovl = makeFpsOverlay(overlayPaint, fps); + } + } + } catch (IOException e) { + Log.e(TAG, "encountered exception during render", e); + } + } + } finally { + if (c != null) { + mSurfaceHolder.unlockCanvasAndPost(c); + } else { + Log.w(TAG, "couldn't unlock surface canvas"); + } + } + } + } + } + } +} diff --git a/ipcam-view/src/main/res/values/attrs.xml b/ipcam-view/src/main/res/values/attrs.xml new file mode 100644 index 000000000..6042ebd0a --- /dev/null +++ b/ipcam-view/src/main/res/values/attrs.xml @@ -0,0 +1,4 @@ + + + + diff --git a/material-intro/build.gradle.kts b/material-intro/build.gradle.kts index ee67915f8..a9f1bf6dd 100644 --- a/material-intro/build.gradle.kts +++ b/material-intro/build.gradle.kts @@ -51,7 +51,7 @@ android { disable += listOf("Overdraw", "OldTargetApi", "GradleDependency") showAll = true warningsAsErrors = true - targetSdk = 34 + targetSdk = versions.getProperty("platformVersions").toInt() } } diff --git a/material-intro/src/main/java/com/heinrichreimersoftware/materialintro/app/IntroActivity.java b/material-intro/src/main/java/com/heinrichreimersoftware/materialintro/app/IntroActivity.java index a679e0b1b..980baf83c 100644 --- a/material-intro/src/main/java/com/heinrichreimersoftware/materialintro/app/IntroActivity.java +++ b/material-intro/src/main/java/com/heinrichreimersoftware/materialintro/app/IntroActivity.java @@ -39,6 +39,7 @@ import android.view.animation.Interpolator; import android.widget.Button; import android.widget.TextSwitcher; +import androidx.activity.OnBackPressedCallback; import androidx.annotation.*; import androidx.appcompat.app.AppCompatActivity; import androidx.constraintlayout.widget.ConstraintLayout; @@ -173,6 +174,22 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.mi_activity_intro); initViews(); + + getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + if (position > 0) { + previousSlide(); + } else { + Intent returnIntent = onSendActivityResult(RESULT_CANCELED); + if (returnIntent != null) + setResult(RESULT_CANCELED, returnIntent); + else + setResult(RESULT_CANCELED); + finish(); + } + } + }); } @Override @@ -220,20 +237,6 @@ protected void onSaveInstanceState(@NonNull Bundle outState) { outState.putBoolean(KEY_BUTTON_CTA_VISIBLE, buttonCtaVisible); } - @Override - public void onBackPressed() { - if (position > 0) { - previousSlide(); - return; - } - Intent returnIntent = onSendActivityResult(RESULT_CANCELED); - if (returnIntent != null) - setResult(RESULT_CANCELED, returnIntent); - else - setResult(RESULT_CANCELED); - super.onBackPressed(); - } - public Intent onSendActivityResult(int result) { return null; } diff --git a/settings.gradle.kts b/settings.gradle.kts index 5b6101bcd..589182d17 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,4 +39,5 @@ dependencyResolutionManagement { } include(":app") -include(":material-intro") \ No newline at end of file +include(":material-intro") +include(":ipcam-view")