diff --git a/res/layout-land/mode_interstitial_layout.xml b/res/layout-land/mode_interstitial_layout.xml index bd63611a68d..14201853c88 100644 --- a/res/layout-land/mode_interstitial_layout.xml +++ b/res/layout-land/mode_interstitial_layout.xml @@ -98,15 +98,28 @@ android:orientation="vertical" app:layout_constraintGuide_percent="0.5" /> - + android:paddingHorizontal="12dp"> + + + + + + /> diff --git a/res/layout/mode_interstitial_layout.xml b/res/layout/mode_interstitial_layout.xml index 74cabdb3715..d0f80a77af7 100644 --- a/res/layout/mode_interstitial_layout.xml +++ b/res/layout/mode_interstitial_layout.xml @@ -43,13 +43,23 @@ android:orientation="vertical"> - + app:layout_constraintTop_toTopOf="parent"> + + + + 4dp 8dp 18dp + + 30dp diff --git a/src/com/android/settings/notification/modes/SetupInterstitialActivity.java b/src/com/android/settings/notification/modes/SetupInterstitialActivity.java index 9d76abbe76e..028e15ba559 100644 --- a/src/com/android/settings/notification/modes/SetupInterstitialActivity.java +++ b/src/com/android/settings/notification/modes/SetupInterstitialActivity.java @@ -16,17 +16,25 @@ package com.android.settings.notification.modes; +import static android.graphics.drawable.GradientDrawable.LINEAR_GRADIENT; +import static android.graphics.drawable.GradientDrawable.Orientation.BL_TR; import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID; import android.app.ActionBar; import android.content.Context; import android.content.Intent; import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; +import android.graphics.Outline; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; import android.os.Bundle; import android.util.Log; import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.view.ViewTreeObserver; import android.widget.Button; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; @@ -50,6 +58,17 @@ public class SetupInterstitialActivity extends FragmentActivity { private static final String TAG = "ModeSetupInterstitial"; private ZenModesBackend mBackend; + private final ViewOutlineProvider mOutlineProvider = new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + // Provides a rounded rectangle outline whose width & height matches the View. + float cornerRadius = getResources().getDimensionPixelSize( + R.dimen.zen_mode_interstitial_corner_radius); + outline.setRoundRect(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight(), + cornerRadius); + } + }; + /** * Returns an intent leading to this page for the given mode and context. */ @@ -127,9 +146,80 @@ public class SetupInterstitialActivity extends FragmentActivity { } } - private void setImage(ImageView img, @NonNull ZenMode mode) { + private void setImage(@NonNull ImageView img, @NonNull ZenMode mode) { + img.setImageDrawable(getModeDrawable(mode)); + img.setClipToOutline(true); + img.setOutlineProvider(mOutlineProvider); + + FrameLayout frame = findViewById(R.id.image_frame); + if (frame == null) { + return; + } + if (img.getMeasuredWidth() == 0) { + // set up to resize after the global layout occurs + img.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + img.getViewTreeObserver().removeOnGlobalLayoutListener(this); + sizeImageToFrame(img, frame); + } + }); + } else { + // measured already, resize it now + sizeImageToFrame(img, frame); + } + } + + private Drawable getModeDrawable(@NonNull ZenMode mode) { // TODO: b/332730534 - set actual images depending on mode type (asynchronously?) - img.setImageDrawable(new ColorDrawable(Color.GRAY)); + GradientDrawable placeholder = new GradientDrawable(); + placeholder.setSize(40, 60); // 4x6 rectangle, slightly taller than wide + placeholder.setGradientType(LINEAR_GRADIENT); + placeholder.setOrientation(BL_TR); + placeholder.setColors(new int[]{Color.BLACK, Color.WHITE}); + return placeholder; + } + + @VisibleForTesting + protected void sizeImageToFrame(ImageView img, FrameLayout frame) { + // width of the space we have available = overall size of frame - relevant padding + int frameHeight = + frame.getMeasuredHeight() - frame.getPaddingTop() - frame.getPaddingBottom(); + int frameWidth = + frame.getMeasuredWidth() - frame.getPaddingLeft() - frame.getPaddingRight(); + + int imgHeight = img.getDrawable().getIntrinsicHeight(); + int imgWidth = img.getDrawable().getIntrinsicWidth(); + + // if any of these are 0, give up because we won't be able to do the relevant math (and + // we probably don't have the relevant data set up) + if (frameHeight == 0 || frameWidth == 0 || imgHeight == 0 || imgWidth == 0) { + Log.w(TAG, "image or frame has invalid size parameters"); + return; + } + float frameHWRatio = ((float) frameHeight) / frameWidth; + float imgHWRatio = ((float) imgHeight) / imgWidth; + + // fit horizontal dimension if the frame has a taller ratio (height/width) than the image; + // otherwise, fit the vertical direction + boolean fitHorizontal = frameHWRatio > imgHWRatio; + + ViewGroup.LayoutParams layoutParams = img.getLayoutParams(); + if (layoutParams == null) { + Log.w(TAG, "image has null LayoutParams"); + return; + } + if (fitHorizontal) { + layoutParams.width = frameWidth; + float scaledHeight = imgHWRatio * frameWidth; + layoutParams.height = (int) scaledHeight; + } else { + layoutParams.height = frameHeight; + float scaledWidth = /* w/h ratio */ (1 / imgHWRatio) * frameHeight; + layoutParams.width = (int) scaledWidth; + } + img.setLayoutParams(layoutParams); } private void setupButton(Button button, @NonNull ZenMode mode) { diff --git a/tests/robotests/src/com/android/settings/notification/modes/SetupInterstitialActivityTest.java b/tests/robotests/src/com/android/settings/notification/modes/SetupInterstitialActivityTest.java index c3ac0fd3083..e73610bd962 100644 --- a/tests/robotests/src/com/android/settings/notification/modes/SetupInterstitialActivityTest.java +++ b/tests/robotests/src/com/android/settings/notification/modes/SetupInterstitialActivityTest.java @@ -23,13 +23,19 @@ import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENT import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.content.Intent; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; import androidx.test.core.app.ActivityScenario; @@ -53,6 +59,15 @@ public class SetupInterstitialActivityTest { @Mock private ZenModesBackend mBackend; + @Mock + private ImageView mImage; + + @Mock + private Drawable mDrawable; + + @Mock + private FrameLayout mFrame; + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -60,6 +75,10 @@ public class SetupInterstitialActivityTest { // set global backend instance so that when the interstitial activity launches, it'll get // this mock backend ZenModesBackend.setInstance(mBackend); + + when(mBackend.getMode(MODE_ID)).thenReturn(new TestModeBuilder().build()); + when(mImage.getDrawable()).thenReturn(mDrawable); + when(mImage.getLayoutParams()).thenReturn(new ViewGroup.LayoutParams(0, 0)); } @Test @@ -96,4 +115,134 @@ public class SetupInterstitialActivityTest { }); scenario.close(); } + + @Test + public void setImageToFrame_sizeZero() { + ActivityScenario scenario = + ActivityScenario.launch(new Intent(Intent.ACTION_MAIN) + .setClass(RuntimeEnvironment.getApplication(), + SetupInterstitialActivity.class) + .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, MODE_ID)); + scenario.onActivity(activity -> { + // when either the image or the frame has a size 0, we do nothing + when(mDrawable.getIntrinsicWidth()).thenReturn(0); + when(mDrawable.getIntrinsicHeight()).thenReturn(25); + when(mFrame.getMeasuredWidth()).thenReturn(40); + when(mFrame.getMeasuredHeight()).thenReturn(50); + + activity.sizeImageToFrame(mImage, mFrame); + verify(mImage, never()).setLayoutParams(any()); + }); + scenario.close(); + } + + @Test + public void setImageToFrame_imageLargerThanFrame() { + ActivityScenario scenario = + ActivityScenario.launch(new Intent(Intent.ACTION_MAIN) + .setClass(RuntimeEnvironment.getApplication(), + SetupInterstitialActivity.class) + .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, MODE_ID)); + scenario.onActivity(activity -> { + // image: 900(w)x1500(h); frame: 600(w)x500(h) + // image expected to be scaled down to match the height of the frame -> 300(w)x500(h) + when(mDrawable.getIntrinsicWidth()).thenReturn(900); + when(mDrawable.getIntrinsicHeight()).thenReturn(1500); + when(mFrame.getMeasuredWidth()).thenReturn(600); + when(mFrame.getMeasuredHeight()).thenReturn(500); + + ArgumentCaptor captor = ArgumentCaptor.forClass( + ViewGroup.LayoutParams.class); + activity.sizeImageToFrame(mImage, mFrame); + verify(mImage).setLayoutParams(captor.capture()); + ViewGroup.LayoutParams out = captor.getValue(); + assertThat(out.width).isEqualTo(300); + assertThat(out.height).isEqualTo(500); + }); + scenario.close(); + } + + @Test + public void setImageToFrame_imageSmallerThanFrame() { + ActivityScenario scenario = + ActivityScenario.launch(new Intent(Intent.ACTION_MAIN) + .setClass(RuntimeEnvironment.getApplication(), + SetupInterstitialActivity.class) + .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, MODE_ID)); + scenario.onActivity(activity -> { + // image: 300(w)x200(h); frame: 900(w)x1200(h) + // image expected to be scaled up to match the width of the frame -> 900(w)x600(h) + when(mDrawable.getIntrinsicWidth()).thenReturn(300); + when(mDrawable.getIntrinsicHeight()).thenReturn(200); + when(mFrame.getMeasuredWidth()).thenReturn(900); + when(mFrame.getMeasuredHeight()).thenReturn(1200); + + ArgumentCaptor captor = ArgumentCaptor.forClass( + ViewGroup.LayoutParams.class); + activity.sizeImageToFrame(mImage, mFrame); + verify(mImage).setLayoutParams(captor.capture()); + ViewGroup.LayoutParams out = captor.getValue(); + assertThat(out.width).isEqualTo(900); + assertThat(out.height).isEqualTo(600); + }); + scenario.close(); + } + + @Test + public void setImageToFrame_horizontalImageNarrowerThanFrame() { + ActivityScenario scenario = + ActivityScenario.launch(new Intent(Intent.ACTION_MAIN) + .setClass(RuntimeEnvironment.getApplication(), + SetupInterstitialActivity.class) + .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, MODE_ID)); + scenario.onActivity(activity -> { + // image: 600(w)x400(h); frame: 1000(w)x100(h) + // both image and frame are wider than tall, but frame is much narrower + // so should fit image to height of frame -> 150(w)x100(h) + when(mDrawable.getIntrinsicWidth()).thenReturn(600); + when(mDrawable.getIntrinsicHeight()).thenReturn(400); + when(mFrame.getMeasuredWidth()).thenReturn(1000); + when(mFrame.getMeasuredHeight()).thenReturn(100); + + ArgumentCaptor captor = ArgumentCaptor.forClass( + ViewGroup.LayoutParams.class); + activity.sizeImageToFrame(mImage, mFrame); + verify(mImage).setLayoutParams(captor.capture()); + ViewGroup.LayoutParams out = captor.getValue(); + assertThat(out.width).isEqualTo(150); + assertThat(out.height).isEqualTo(100); + }); + scenario.close(); + } + + @Test + public void setImageToFrame_accountsForPadding() { + ActivityScenario scenario = + ActivityScenario.launch(new Intent(Intent.ACTION_MAIN) + .setClass(RuntimeEnvironment.getApplication(), + SetupInterstitialActivity.class) + .putExtra(EXTRA_AUTOMATIC_ZEN_RULE_ID, MODE_ID)); + scenario.onActivity(activity -> { + // image: 200(w)x300(h); frame: 1000(w)x1000(h), 50 top/bottom padding, 100 l/r padding + // effective size of frame is therefore 800(w)x900(h) + // scale image to the height of the effective frame -> 600(w)x900(h) + when(mDrawable.getIntrinsicWidth()).thenReturn(200); + when(mDrawable.getIntrinsicHeight()).thenReturn(300); + when(mFrame.getMeasuredWidth()).thenReturn(1000); + when(mFrame.getMeasuredHeight()).thenReturn(1000); + when(mFrame.getPaddingTop()).thenReturn(50); + when(mFrame.getPaddingBottom()).thenReturn(50); + when(mFrame.getPaddingLeft()).thenReturn(100); + when(mFrame.getPaddingRight()).thenReturn(100); + + ArgumentCaptor captor = ArgumentCaptor.forClass( + ViewGroup.LayoutParams.class); + activity.sizeImageToFrame(mImage, mFrame); + verify(mImage).setLayoutParams(captor.capture()); + ViewGroup.LayoutParams out = captor.getValue(); + assertThat(out.width).isEqualTo(600); + assertThat(out.height).isEqualTo(900); + }); + scenario.close(); + } }