DO NOT MERGE: Show an onboarding screen for priority conversations

Test: atest SystemUITests
Bug: 151843296
Change-Id: I5280cff71591f8551016b6ba00d3a579507367cb
This commit is contained in:
Evan Laird
2020-04-08 17:45:24 -04:00
parent 23e062deb5
commit 31ca547400
11 changed files with 666 additions and 14 deletions

View File

@@ -0,0 +1,194 @@
<!--
~ Copyright (C) 2020 The Android Open Source Project
~
~ 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
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/onboarding_half_shell_container"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom"
android:paddingStart="4dp"
android:paddingEnd="4dp"
>
<LinearLayout
android:id="@+id/half_shell"
android:layout_width="@dimen/qs_panel_width"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:orientation="vertical"
android:gravity="bottom"
android:layout_gravity="center_horizontal|bottom"
android:background="@drawable/rounded_bg_full"
>
<!-- We have a known number of rows that can be shown; just design them all here -->
<LinearLayout
android:id="@+id/show_at_top_tip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp"
android:orientation="horizontal"
>
<ImageView
android:id="@+id/bell_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_notifications_alert"
android:tint="?android:attr/colorControlNormal" />
<TextView
android:id="@+id/show_at_top_text"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:gravity="center_vertical|start"
android:textSize="15sp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/priority_onboarding_show_at_top_text"
style="@style/TextAppearance.NotificationInfo"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/show_avatar_tip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp"
android:orientation="horizontal"
>
<ImageView
android:id="@+id/avatar_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_person"
android:tint="?android:attr/colorControlNormal" />
<TextView
android:id="@+id/avatar_text"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:gravity="center_vertical|start"
android:textSize="15sp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/priority_onboarding_show_avatar_text"
style="@style/TextAppearance.NotificationInfo"
/>
</LinearLayout>
<!-- These rows show optionally -->
<LinearLayout
android:id="@+id/floating_bubble_tip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp"
android:orientation="horizontal"
>
<ImageView
android:id="@+id/bubble_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_create_bubble"
android:tint="?android:attr/colorControlNormal" />
<TextView
android:id="@+id/bubble_text"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:gravity="center_vertical|start"
android:textSize="15sp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/priority_onboarding_appear_as_bubble_text"
style="@style/TextAppearance.NotificationInfo"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/ignore_dnd_tip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp"
android:orientation="horizontal"
>
<ImageView
android:id="@+id/dnd_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:src="@drawable/moon"
android:tint="?android:attr/colorControlNormal" />
<TextView
android:id="@+id/dnd_text"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:gravity="center_vertical|start"
android:textSize="15sp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/priority_onboarding_ignores_dnd_text"
style="@style/TextAppearance.NotificationInfo"
/>
</LinearLayout>
<!-- Bottom button container -->
<RelativeLayout
android:id="@+id/button_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp"
android:orientation="horizontal"
>
<TextView
android:id="@+id/done_button"
android:text="@string/priority_onboarding_done_button_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:gravity="end|center_vertical"
android:minWidth="@dimen/notification_importance_toggle_size"
android:minHeight="@dimen/notification_importance_toggle_size"
android:maxWidth="125dp"
style="@style/TextAppearance.NotificationInfo.Button"/>
</RelativeLayout>
</LinearLayout>
</FrameLayout>

View File

@@ -2639,6 +2639,18 @@
<!-- Title of the overlay warning the user to interact with the device or it will go to sleep. [CHAR LIMIT=25] -->
<string name="inattentive_sleep_warning_title">Standby</string>
<!-- Priority conversation onboarding screen -->
<!-- Text explaining that priority conversations show at the top of the conversation section [CHAR LIMIT=50] -->
<string name="priority_onboarding_show_at_top_text">Show at top of conversation section</string>
<!-- Text explaining that priority conversations show an avatar on the lock screen [CHAR LIMIT=50] -->
<string name="priority_onboarding_show_avatar_text">Show profile picture on lock screen</string>
<!-- Text explaining that priority conversations will appear as a bubble [CHAR LIMIT=50] -->
<string name="priority_onboarding_appear_as_bubble_text">Appear as a floating bubble on top of apps</string>
<!-- Text explaining that priority conversations can interrupt DnD settings [CHAR LIMIT=50] -->
<string name="priority_onboarding_ignores_dnd_text">Interrupt Do Not Disturb</string>
<!-- Title for the affirmative button [CHAR LIMIT=50] -->
<string name="priority_onboarding_done_button_title">Got it</string>
<!-- Window Magnification strings -->
<!-- Title for Magnification Overlay Window [CHAR LIMIT=NONE] -->
<string name="magnification_overlay_title">Magnification Overlay Window</string>

View File

@@ -21,11 +21,25 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import com.android.systemui.settings.CurrentUserContextTracker;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Map;
import java.util.Set;
/**
* A helper class to store simple preferences for SystemUI. Its main use case is things such as
* feature education, e.g. "has the user seen this tooltip".
*
* As of this writing, feature education settings are *intentionally exempted* from backup and
* restore because there is not a great way to know which subset of features the user _should_ see
* again if, for instance, they are coming from multiple OSes back or switching OEMs.
*
* NOTE: Clients of this class should take care to pass in the correct user context when querying
* settings, otherwise you will always read/write for user 0 which is almost never what you want.
* See {@link CurrentUserContextTracker} for a simple way to get the current context
*/
public final class Prefs {
private Prefs() {} // no instantation
@@ -109,6 +123,8 @@ public final class Prefs {
String HAS_SEEN_BUBBLES_EDUCATION = "HasSeenBubblesOnboarding";
String HAS_SEEN_BUBBLES_MANAGE_EDUCATION = "HasSeenBubblesManageOnboarding";
String CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT = "ControlsStructureSwipeTooltipCount";
/** Tracks whether the user has seen the onboarding screen for priority conversations */
String HAS_SEEN_PRIORITY_ONBOARDING = "HasSeenPriorityOnboarding";
}
public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) {

View File

@@ -24,6 +24,7 @@ import android.content.Context;
import androidx.annotation.Nullable;
import com.android.keyguard.KeyguardViewController;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dock.DockManager;
import com.android.systemui.dock.DockManagerImpl;
import com.android.systemui.plugins.qs.QSFactory;
@@ -33,6 +34,7 @@ import com.android.systemui.power.EnhancedEstimatesImpl;
import com.android.systemui.qs.tileimpl.QSFactoryImpl;
import com.android.systemui.recents.Recents;
import com.android.systemui.recents.RecentsImplementation;
import com.android.systemui.settings.CurrentUserContextTracker;
import com.android.systemui.stackdivider.DividerModule;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
@@ -136,4 +138,15 @@ public abstract class SystemUIDefaultModule {
@Binds
abstract KeyguardViewController bindKeyguardViewController(
StatusBarKeyguardViewManager statusBarKeyguardViewManager);
@Singleton
@Provides
static CurrentUserContextTracker provideCurrentUserContextTracker(
Context context,
BroadcastDispatcher broadcastDispatcher) {
CurrentUserContextTracker tracker =
new CurrentUserContextTracker(context, broadcastDispatcher);
tracker.initialize();
return tracker;
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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.android.systemui.settings
import android.content.Context
import android.os.UserHandle
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.util.Assert
import javax.inject.Inject
import javax.inject.Singleton
/**
* Tracks a reference to the context for the current user
*/
@Singleton
class CurrentUserContextTracker @Inject constructor(
private val sysuiContext: Context,
broadcastDispatcher: BroadcastDispatcher
) {
private val userTracker: CurrentUserTracker
var currentUserContext: Context
init {
userTracker = object : CurrentUserTracker(broadcastDispatcher) {
override fun onUserSwitched(newUserId: Int) {
handleUserSwitched(newUserId)
}
}
currentUserContext = makeUserContext(userTracker.currentUserId)
}
fun initialize() {
userTracker.startTracking()
}
private fun handleUserSwitched(newUserId: Int) {
currentUserContext = makeUserContext(newUserId)
}
private fun makeUserContext(uid: Int): Context {
Assert.isMainThread()
return sysuiContext.createContextAsUser(
UserHandle.getUserHandleForUid(userTracker.currentUserId), 0)
}
}

View File

@@ -30,6 +30,7 @@ import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.UiBackground;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.settings.CurrentUserContextTracker;
import com.android.systemui.statusbar.FeatureFlags;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
@@ -53,6 +54,7 @@ import com.android.systemui.statusbar.notification.logging.NotificationPanelLogg
import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerImpl;
import com.android.systemui.statusbar.notification.row.NotificationBlockingHelperManager;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.row.PriorityOnboardingDialogController;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -60,6 +62,7 @@ import com.android.systemui.util.leak.LeakDetector;
import java.util.concurrent.Executor;
import javax.inject.Provider;
import javax.inject.Singleton;
import dagger.Binds;
@@ -109,7 +112,9 @@ public interface NotificationsModule {
HighPriorityProvider highPriorityProvider,
INotificationManager notificationManager,
LauncherApps launcherApps,
ShortcutManager shortcutManager) {
ShortcutManager shortcutManager,
CurrentUserContextTracker contextTracker,
Provider<PriorityOnboardingDialogController.Builder> builderProvider) {
return new NotificationGutsManager(
context,
visualStabilityManager,
@@ -119,7 +124,9 @@ public interface NotificationsModule {
highPriorityProvider,
notificationManager,
launcherApps,
shortcutManager);
shortcutManager,
contextTracker,
builderProvider);
}
/** Provides an instance of {@link VisualStabilityManager} */

View File

@@ -23,6 +23,7 @@ import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_LOW;
import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
import static android.provider.Settings.Global.NOTIFICATION_BUBBLES;
import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
@@ -45,6 +46,7 @@ import android.graphics.drawable.Icon;
import android.os.Handler;
import android.os.Parcelable;
import android.os.RemoteException;
import android.provider.Settings;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.transition.ChangeBounds;
@@ -53,7 +55,7 @@ import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Slog;
import android.view.LayoutInflater;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.widget.ImageView;
@@ -63,6 +65,7 @@ import android.widget.TextView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settingslib.notification.ConversationIconFactory;
import com.android.systemui.Dependency;
import com.android.systemui.Prefs;
import com.android.systemui.R;
import com.android.systemui.statusbar.notification.NotificationChannelHelper;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
@@ -71,6 +74,8 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import java.lang.annotation.Retention;
import java.util.List;
import javax.inject.Provider;
/**
* The guts of a conversation notification revealed when performing a long press.
*/
@@ -93,6 +98,9 @@ public class NotificationConversationInfo extends LinearLayout implements
private ShortcutInfo mShortcutInfo;
private String mConversationId;
private StatusBarNotification mSbn;
private Notification.BubbleMetadata mBubbleMetadata;
private Context mUserContext;
private Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider;
private boolean mIsDeviceProvisioned;
private int mAppBubble;
@@ -136,17 +144,17 @@ public class NotificationConversationInfo extends LinearLayout implements
*/
private OnClickListener mOnFavoriteClick = v -> {
mSelectedAction = ACTION_FAVORITE;
setSelectedAction(ACTION_FAVORITE);
updateToggleActions(mSelectedAction, true);
};
private OnClickListener mOnDefaultClick = v -> {
mSelectedAction = ACTION_DEFAULT;
setSelectedAction(ACTION_DEFAULT);
updateToggleActions(mSelectedAction, true);
};
private OnClickListener mOnMuteClick = v -> {
mSelectedAction = ACTION_MUTE;
setSelectedAction(ACTION_MUTE);
updateToggleActions(mSelectedAction, true);
};
@@ -170,6 +178,23 @@ public class NotificationConversationInfo extends LinearLayout implements
void onClick(View v, int hoursToSnooze);
}
@VisibleForTesting
void setSelectedAction(int selectedAction) {
if (mSelectedAction == selectedAction) {
return;
}
mSelectedAction = selectedAction;
onSelectedActionChanged();
}
private void onSelectedActionChanged() {
// If the user selected Priority, maybe show the priority onboarding
if (mSelectedAction == ACTION_FAVORITE && shouldShowPriorityOnboarding()) {
showPriorityOnboarding();
}
}
public void bindNotification(
ShortcutManager shortcutManager,
PackageManager pm,
@@ -181,6 +206,8 @@ public class NotificationConversationInfo extends LinearLayout implements
OnSettingsClickListener onSettingsClick,
OnSnoozeClickListener onSnoozeClickListener,
ConversationIconFactory conversationIconFactory,
Context userContext,
Provider<PriorityOnboardingDialogController.Builder> builderProvider,
boolean isDeviceProvisioned) {
mSelectedAction = -1;
mINotificationManager = iNotificationManager;
@@ -196,6 +223,9 @@ public class NotificationConversationInfo extends LinearLayout implements
mIsDeviceProvisioned = isDeviceProvisioned;
mOnSnoozeClickListener = onSnoozeClickListener;
mIconFactory = conversationIconFactory;
mUserContext = userContext;
mBubbleMetadata = entry.getBubbleMetadata();
mBuilderProvider = builderProvider;
mShortcutManager = shortcutManager;
mConversationId = mNotificationChannel.getConversationId();
@@ -213,7 +243,7 @@ public class NotificationConversationInfo extends LinearLayout implements
try {
mAppBubble = mINotificationManager.getBubblePreferenceForPackage(mPackageName, mAppUid);
} catch (RemoteException e) {
Slog.e(TAG, "can't reach OS", e);
Log.e(TAG, "can't reach OS", e);
mAppBubble = BUBBLE_PREFERENCE_SELECTED;
}
@@ -491,6 +521,38 @@ public class NotificationConversationInfo extends LinearLayout implements
mAppUid, mSelectedAction, mNotificationChannel));
}
private boolean shouldShowPriorityOnboarding() {
return !Prefs.getBoolean(mUserContext, Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING, false);
}
private void showPriorityOnboarding() {
View onboardingView = LayoutInflater.from(mContext)
.inflate(R.layout.priority_onboarding_half_shell, null);
boolean ignoreDnd = false;
try {
ignoreDnd = (mINotificationManager
.getConsolidatedNotificationPolicy().priorityConversationSenders
& NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT) != 0;
} catch (RemoteException e) {
Log.e(TAG, "Could not check conversation senders", e);
}
boolean showAsBubble = mBubbleMetadata.getAutoExpandBubble()
&& Settings.Global.getInt(mContext.getContentResolver(),
NOTIFICATION_BUBBLES, 0) == 1;
PriorityOnboardingDialogController controller = mBuilderProvider.get()
.setContext(mUserContext)
.setView(onboardingView)
.setIgnoresDnd(ignoreDnd)
.setShowsAsBubble(showAsBubble)
.build();
controller.init();
controller.show();
}
/**
* Closes the controls and commits the updated importance values (indirectly).
*

View File

@@ -49,6 +49,7 @@ import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.settings.CurrentUserContextTracker;
import com.android.systemui.statusbar.NotificationLifetimeExtender;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationPresenter;
@@ -67,6 +68,8 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import javax.inject.Provider;
import dagger.Lazy;
/**
@@ -111,6 +114,8 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx
private final INotificationManager mNotificationManager;
private final LauncherApps mLauncherApps;
private final ShortcutManager mShortcutManager;
private final CurrentUserContextTracker mContextTracker;
private final Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider;
/**
* Injected constructor. See {@link NotificationsModule}.
@@ -121,7 +126,9 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx
HighPriorityProvider highPriorityProvider,
INotificationManager notificationManager,
LauncherApps launcherApps,
ShortcutManager shortcutManager) {
ShortcutManager shortcutManager,
CurrentUserContextTracker contextTracker,
Provider<PriorityOnboardingDialogController.Builder> builderProvider) {
mContext = context;
mVisualStabilityManager = visualStabilityManager;
mStatusBarLazy = statusBarLazy;
@@ -131,6 +138,8 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx
mNotificationManager = notificationManager;
mLauncherApps = launcherApps;
mShortcutManager = shortcutManager;
mContextTracker = contextTracker;
mBuilderProvider = builderProvider;
}
public void setUpWithPresenter(NotificationPresenter presenter,
@@ -403,6 +412,8 @@ public class NotificationGutsManager implements Dumpable, NotificationLifetimeEx
onSettingsClick,
onSnoozeClickListener,
iconFactoryLoader,
mContextTracker.getCurrentUserContext(),
mBuilderProvider,
mDeviceProvisionedController.isDeviceProvisioned());
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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.android.systemui.statusbar.notification.row
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.graphics.PixelFormat
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.View
import android.view.View.GONE
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.Window
import android.view.WindowInsets.Type.statusBars
import android.view.WindowManager
import android.widget.LinearLayout
import android.widget.TextView
import com.android.systemui.Prefs
import com.android.systemui.R
import java.lang.IllegalStateException
import javax.inject.Inject
/**
* Controller to handle presenting the priority conversations onboarding dialog
*/
class PriorityOnboardingDialogController @Inject constructor(
val view: View,
val context: Context,
val ignoresDnd: Boolean,
val showsAsBubble: Boolean
) {
private lateinit var dialog: Dialog
fun init() {
initDialog()
}
fun show() {
dialog.show()
}
private fun done() {
// Log that the user has seen the onboarding
Prefs.putBoolean(context, Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING, true)
dialog.dismiss()
}
class Builder @Inject constructor() {
private lateinit var view: View
private lateinit var context: Context
private var ignoresDnd = false
private var showAsBubble = false
fun setView(v: View): Builder {
view = v
return this
}
fun setContext(c: Context): Builder {
context = c
return this
}
fun setIgnoresDnd(ignore: Boolean): Builder {
ignoresDnd = ignore
return this
}
fun setShowsAsBubble(bubble: Boolean): Builder {
showAsBubble = bubble
return this
}
fun build(): PriorityOnboardingDialogController {
val controller = PriorityOnboardingDialogController(
view, context, ignoresDnd, showAsBubble)
return controller
}
}
private fun initDialog() {
dialog = Dialog(context)
if (dialog.window == null) {
throw IllegalStateException("Need a window for the onboarding dialog to show")
}
dialog.window?.requestFeature(Window.FEATURE_NO_TITLE)
// Prevent a11y readers from reading the first element in the dialog twice
dialog.setTitle("\u00A0")
dialog.apply {
setContentView(view)
setCanceledOnTouchOutside(true)
findViewById<TextView>(R.id.done_button)?.setOnClickListener {
done()
}
if (!ignoresDnd) {
findViewById<LinearLayout>(R.id.ignore_dnd_tip).visibility = GONE
}
if (!showsAsBubble) {
findViewById<LinearLayout>(R.id.floating_bubble_tip).visibility = GONE
}
window?.apply {
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
addFlags(wmFlags)
setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
setWindowAnimations(com.android.internal.R.style.Animation_InputMethod)
attributes = attributes.apply {
format = PixelFormat.TRANSLUCENT
title = ChannelEditorDialogController::class.java.simpleName
gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
fitInsetsTypes = attributes.fitInsetsTypes and statusBars().inv()
width = MATCH_PARENT
height = WRAP_CONTENT
}
}
}
}
private val wmFlags = (WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
}

View File

@@ -16,15 +16,12 @@
package com.android.systemui.statusbar.notification.row;
import static android.app.Notification.FLAG_BUBBLE;
import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL;
import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.IMPORTANCE_HIGH;
import static android.app.NotificationManager.IMPORTANCE_LOW;
import static android.print.PrintManager.PRINT_SPOOLER_PACKAGE_NAME;
import static android.provider.Settings.Global.NOTIFICATION_BUBBLES;
import static android.provider.Settings.Secure.BUBBLE_IMPORTANT_CONVERSATIONS;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
@@ -38,8 +35,10 @@ import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -51,6 +50,7 @@ import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.PendingIntent;
import android.app.Person;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.LauncherApps;
@@ -61,20 +61,19 @@ import android.content.pm.ShortcutManager;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.notification.StatusBarNotification;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.internal.logging.MetricsLogger;
import com.android.settingslib.notification.ConversationIconFactory;
import com.android.systemui.Dependency;
import com.android.systemui.Prefs;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.bubbles.BubbleController;
@@ -89,6 +88,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
@@ -99,6 +99,8 @@ import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import javax.inject.Provider;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@@ -145,6 +147,11 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
private ShadeController mShadeController;
@Mock
private ConversationIconFactory mIconFactory;
@Mock
private Context mUserContext;
@Mock(answer = Answers.RETURNS_SELF)
private PriorityOnboardingDialogController.Builder mBuilder;
private Provider<PriorityOnboardingDialogController.Builder> mBuilderProvider = () -> mBuilder;
@Before
public void setUp() throws Exception {
@@ -236,6 +243,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
final ImageView view = mNotificationInfo.findViewById(R.id.conversation_icon);
assertEquals(mIconDrawable, view.getDrawable());
@@ -255,6 +264,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name);
assertTrue(textView.getText().toString().contains("App Name"));
@@ -300,6 +311,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
final TextView textView = mNotificationInfo.findViewById(R.id.group_name);
assertTrue(textView.getText().toString().contains(group.getName()));
@@ -321,6 +334,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
final TextView textView = mNotificationInfo.findViewById(R.id.group_name);
assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility());
@@ -341,6 +356,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
assertEquals(GONE, nameView.getVisibility());
@@ -368,6 +385,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
assertEquals(VISIBLE, nameView.getVisibility());
@@ -391,6 +410,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
},
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
final View settingsButton = mNotificationInfo.findViewById(R.id.info);
@@ -412,6 +433,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
final View settingsButton = mNotificationInfo.findViewById(R.id.info);
assertTrue(settingsButton.getVisibility() != View.VISIBLE);
@@ -434,6 +457,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
},
null,
mIconFactory,
mUserContext,
mBuilderProvider,
false);
final View settingsButton = mNotificationInfo.findViewById(R.id.info);
assertTrue(settingsButton.getVisibility() != View.VISIBLE);
@@ -454,6 +479,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
View view = mNotificationInfo.findViewById(R.id.silence);
assertThat(view.isSelected()).isTrue();
@@ -477,6 +504,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
View view = mNotificationInfo.findViewById(R.id.default_behavior);
assertThat(view.isSelected()).isTrue();
@@ -503,6 +532,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
View view = mNotificationInfo.findViewById(R.id.default_behavior);
assertThat(view.isSelected()).isTrue();
@@ -528,6 +559,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
View fave = mNotificationInfo.findViewById(R.id.priority);
@@ -566,6 +599,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
mNotificationInfo.findViewById(R.id.default_behavior).performClick();
@@ -603,6 +638,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
View silence = mNotificationInfo.findViewById(R.id.silence);
@@ -641,6 +678,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
View fave = mNotificationInfo.findViewById(R.id.priority);
@@ -673,6 +712,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
View fave = mNotificationInfo.findViewById(R.id.priority);
@@ -703,6 +744,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
mNotificationInfo.findViewById(R.id.default_behavior).performClick();
@@ -734,6 +777,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
mNotificationInfo.findViewById(R.id.default_behavior).performClick();
@@ -765,6 +810,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
mNotificationInfo.findViewById(R.id.default_behavior).performClick();
@@ -795,6 +842,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
View silence = mNotificationInfo.findViewById(R.id.silence);
@@ -824,6 +873,8 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
verify(mMockINotificationManager, times(1)).createConversationNotificationChannelForPackage(
@@ -844,9 +895,81 @@ public class NotificationConversationInfoTest extends SysuiTestCase {
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
verify(mMockINotificationManager, never()).createConversationNotificationChannelForPackage(
anyString(), anyInt(), anyString(), any(), eq(CONVERSATION_ID));
}
@Test
public void testSelectPriorityPresentsOnboarding_firstTime() {
// GIVEN pref is false
Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING, false);
// GIVEN the priority onboarding screen is present
PriorityOnboardingDialogController.Builder b =
new PriorityOnboardingDialogController.Builder();
PriorityOnboardingDialogController controller =
mock(PriorityOnboardingDialogController.class);
when(b.build()).thenReturn(controller);
// GIVEN the user is changing conversation settings
when(mBuilderProvider.get()).thenReturn(b);
mNotificationInfo.bindNotification(
mShortcutManager,
mMockPackageManager,
mMockINotificationManager,
mVisualStabilityManager,
TEST_PACKAGE_NAME,
mNotificationChannel,
mEntry,
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
// WHEN user clicks "priority"
mNotificationInfo.setSelectedAction(NotificationConversationInfo.ACTION_FAVORITE);
// THEN the user is presented with the priority onboarding screen
verify(controller, atLeastOnce()).show();
}
@Test
public void testSelectPriorityDoesNotShowOnboarding_secondTime() {
//WHEN pref is true
Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_PRIORITY_ONBOARDING, true);
PriorityOnboardingDialogController.Builder b =
new PriorityOnboardingDialogController.Builder();
PriorityOnboardingDialogController controller =
mock(PriorityOnboardingDialogController.class);
when(b.build()).thenReturn(controller);
when(mBuilderProvider.get()).thenReturn(b);
mNotificationInfo.bindNotification(
mShortcutManager,
mMockPackageManager,
mMockINotificationManager,
mVisualStabilityManager,
TEST_PACKAGE_NAME,
mNotificationChannel,
mEntry,
null,
null,
mIconFactory,
mUserContext,
mBuilderProvider,
true);
// WHEN user clicks "priority"
mNotificationInfo.setSelectedAction(NotificationConversationInfo.ACTION_FAVORITE);
// THEN the user is presented with the priority onboarding screen
verify(controller, never()).show();
}
}

View File

@@ -66,6 +66,7 @@ import androidx.test.filters.SmallTest;
import com.android.internal.logging.MetricsLogger;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.settings.CurrentUserContextTracker;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.notification.NotificationActivityStarter;
@@ -83,11 +84,14 @@ import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import javax.inject.Provider;
/**
* Tests for {@link NotificationGutsManager}.
*/
@@ -120,6 +124,10 @@ public class NotificationGutsManagerTest extends SysuiTestCase {
@Mock private LauncherApps mLauncherApps;
@Mock private ShortcutManager mShortcutManager;
@Mock private PeopleNotificationIdentifier mPeopleNotificationIdentifier;
@Mock private CurrentUserContextTracker mContextTracker;
@Mock(answer = Answers.RETURNS_SELF)
private PriorityOnboardingDialogController.Builder mBuilder;
private Provider<PriorityOnboardingDialogController.Builder> mProvider = () -> mBuilder;
@Before
public void setUp() {
@@ -136,7 +144,7 @@ public class NotificationGutsManagerTest extends SysuiTestCase {
mGutsManager = new NotificationGutsManager(mContext, mVisualStabilityManager,
() -> mStatusBar, mHandler, mAccessibilityManager, mHighPriorityProvider,
mINotificationManager, mLauncherApps, mShortcutManager);
mINotificationManager, mLauncherApps, mShortcutManager, mContextTracker, mProvider);
mGutsManager.setUpWithPresenter(mPresenter, mStackScroller,
mCheckSaveListener, mOnSettingsClickListener);
mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter);