Merge changes I71eb6e23,I6cd669ee into tm-qpr-dev
* changes:
Fix lint warnings in KeyguardUpdateMonitor
Fix lint errors in KeyguardUpdateMonitor.
diff --git a/cmds/idmap2/idmap2d/Idmap2Service.cpp b/cmds/idmap2/idmap2d/Idmap2Service.cpp
index 1b2d905..e263810 100644
--- a/cmds/idmap2/idmap2d/Idmap2Service.cpp
+++ b/cmds/idmap2/idmap2d/Idmap2Service.cpp
@@ -23,6 +23,7 @@
#include <cstring>
#include <filesystem>
#include <fstream>
+#include <limits>
#include <memory>
#include <ostream>
#include <string>
@@ -295,26 +296,42 @@
return ok();
}
-Status Idmap2Service::acquireFabricatedOverlayIterator() {
+Status Idmap2Service::acquireFabricatedOverlayIterator(int32_t* _aidl_return) {
+ std::lock_guard l(frro_iter_mutex_);
if (frro_iter_.has_value()) {
LOG(WARNING) << "active ffro iterator was not previously released";
}
frro_iter_ = std::filesystem::directory_iterator(kIdmapCacheDir);
+ if (frro_iter_id_ == std::numeric_limits<int32_t>::max()) {
+ frro_iter_id_ = 0;
+ } else {
+ ++frro_iter_id_;
+ }
+ *_aidl_return = frro_iter_id_;
return ok();
}
-Status Idmap2Service::releaseFabricatedOverlayIterator() {
+Status Idmap2Service::releaseFabricatedOverlayIterator(int32_t iteratorId) {
+ std::lock_guard l(frro_iter_mutex_);
if (!frro_iter_.has_value()) {
LOG(WARNING) << "no active ffro iterator to release";
+ } else if (frro_iter_id_ != iteratorId) {
+ LOG(WARNING) << "incorrect iterator id in a call to release";
+ } else {
+ frro_iter_.reset();
}
return ok();
}
-Status Idmap2Service::nextFabricatedOverlayInfos(
+Status Idmap2Service::nextFabricatedOverlayInfos(int32_t iteratorId,
std::vector<os::FabricatedOverlayInfo>* _aidl_return) {
+ std::lock_guard l(frro_iter_mutex_);
+
constexpr size_t kMaxEntryCount = 100;
if (!frro_iter_.has_value()) {
return error("no active frro iterator");
+ } else if (frro_iter_id_ != iteratorId) {
+ return error("incorrect iterator id in a call to next");
}
size_t count = 0;
@@ -322,22 +339,22 @@
auto entry_iter_end = end(*frro_iter_);
for (; entry_iter != entry_iter_end && count < kMaxEntryCount; ++entry_iter) {
auto& entry = *entry_iter;
- if (!entry.is_regular_file() || !android::IsFabricatedOverlay(entry.path())) {
+ if (!entry.is_regular_file() || !android::IsFabricatedOverlay(entry.path().native())) {
continue;
}
- const auto overlay = FabricatedOverlayContainer::FromPath(entry.path());
+ const auto overlay = FabricatedOverlayContainer::FromPath(entry.path().native());
if (!overlay) {
LOG(WARNING) << "Failed to open '" << entry.path() << "': " << overlay.GetErrorMessage();
continue;
}
- const auto info = (*overlay)->GetManifestInfo();
+ auto info = (*overlay)->GetManifestInfo();
os::FabricatedOverlayInfo out_info;
- out_info.packageName = info.package_name;
- out_info.overlayName = info.name;
- out_info.targetPackageName = info.target_package;
- out_info.targetOverlayable = info.target_name;
+ out_info.packageName = std::move(info.package_name);
+ out_info.overlayName = std::move(info.name);
+ out_info.targetPackageName = std::move(info.target_package);
+ out_info.targetOverlayable = std::move(info.target_name);
out_info.path = entry.path();
_aidl_return->emplace_back(std::move(out_info));
count++;
diff --git a/cmds/idmap2/idmap2d/Idmap2Service.h b/cmds/idmap2/idmap2d/Idmap2Service.h
index c61e4bc..cc8cc5f 100644
--- a/cmds/idmap2/idmap2d/Idmap2Service.h
+++ b/cmds/idmap2/idmap2d/Idmap2Service.h
@@ -26,7 +26,10 @@
#include <filesystem>
#include <memory>
+#include <mutex>
+#include <optional>
#include <string>
+#include <variant>
#include <vector>
namespace android::os {
@@ -60,11 +63,11 @@
binder::Status deleteFabricatedOverlay(const std::string& overlay_path,
bool* _aidl_return) override;
- binder::Status acquireFabricatedOverlayIterator() override;
+ binder::Status acquireFabricatedOverlayIterator(int32_t* _aidl_return) override;
- binder::Status releaseFabricatedOverlayIterator() override;
+ binder::Status releaseFabricatedOverlayIterator(int32_t iteratorId) override;
- binder::Status nextFabricatedOverlayInfos(
+ binder::Status nextFabricatedOverlayInfos(int32_t iteratorId,
std::vector<os::FabricatedOverlayInfo>* _aidl_return) override;
binder::Status dumpIdmap(const std::string& overlay_path, std::string* _aidl_return) override;
@@ -74,7 +77,9 @@
// be able to be recalculated if idmap2 dies and restarts.
std::unique_ptr<idmap2::TargetResourceContainer> framework_apk_cache_;
+ int32_t frro_iter_id_ = 0;
std::optional<std::filesystem::directory_iterator> frro_iter_;
+ std::mutex frro_iter_mutex_;
template <typename T>
using MaybeUniquePtr = std::variant<std::unique_ptr<T>, T*>;
diff --git a/cmds/idmap2/idmap2d/aidl/services/android/os/IIdmap2.aidl b/cmds/idmap2/idmap2d/aidl/services/android/os/IIdmap2.aidl
index 0059cf2..2bbfba9 100644
--- a/cmds/idmap2/idmap2d/aidl/services/android/os/IIdmap2.aidl
+++ b/cmds/idmap2/idmap2d/aidl/services/android/os/IIdmap2.aidl
@@ -41,9 +41,9 @@
@nullable FabricatedOverlayInfo createFabricatedOverlay(in FabricatedOverlayInternal overlay);
boolean deleteFabricatedOverlay(@utf8InCpp String path);
- void acquireFabricatedOverlayIterator();
- void releaseFabricatedOverlayIterator();
- List<FabricatedOverlayInfo> nextFabricatedOverlayInfos();
+ int acquireFabricatedOverlayIterator();
+ void releaseFabricatedOverlayIterator(int iteratorId);
+ List<FabricatedOverlayInfo> nextFabricatedOverlayInfos(int iteratorId);
@utf8InCpp String dumpIdmap(@utf8InCpp String overlayApkPath);
}
diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java
index f0e1448..aa5fa5b 100644
--- a/core/java/android/app/BroadcastOptions.java
+++ b/core/java/android/app/BroadcastOptions.java
@@ -528,6 +528,28 @@
return mIsAlarmBroadcast;
}
+ /**
+ * Did this broadcast originate from a push message from the server?
+ *
+ * @return true if this broadcast is a push message, false otherwise.
+ * @hide
+ */
+ public boolean isPushMessagingBroadcast() {
+ return mTemporaryAppAllowlistReasonCode == PowerExemptionManager.REASON_PUSH_MESSAGING;
+ }
+
+ /**
+ * Did this broadcast originate from a push message from the server which was over the allowed
+ * quota?
+ *
+ * @return true if this broadcast is a push message over quota, false otherwise.
+ * @hide
+ */
+ public boolean isPushMessagingOverQuotaBroadcast() {
+ return mTemporaryAppAllowlistReasonCode
+ == PowerExemptionManager.REASON_PUSH_MESSAGING_OVER_QUOTA;
+ }
+
/** {@hide} */
public long getRequireCompatChangeId() {
return mRequireCompatChangeId;
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index 58f9336..26e5e95 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -34,6 +34,7 @@
import org.xmlpull.v1.XmlPullParserException;
import java.io.Closeable;
+import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -449,6 +450,16 @@
return proto.getBytes();
}
+ /**
+ * Writes contents in a binary protobuffer format, using
+ * the android.os.BatteryUsageStatsAtomsProto proto.
+ */
+ public void dumpToProto(FileDescriptor fd) {
+ final ProtoOutputStream proto = new ProtoOutputStream(fd);
+ writeStatsProto(proto, /* max size */ Integer.MAX_VALUE);
+ proto.flush();
+ }
+
@NonNull
private void writeStatsProto(ProtoOutputStream proto, int maxRawSize) {
final BatteryConsumer deviceBatteryConsumer = getAggregateBatteryConsumer(
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 620f177..f5ba275 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -1114,6 +1114,19 @@
resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS);
resolveIntent.setComponent(cn);
resolveIntent.setAction(Intent.ACTION_EDIT);
+ String originalAction = originalIntent.getAction();
+ if (Intent.ACTION_SEND.equals(originalAction)) {
+ if (resolveIntent.getData() == null) {
+ Uri uri = resolveIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (uri != null) {
+ String mimeType = getContentResolver().getType(uri);
+ resolveIntent.setDataAndType(uri, mimeType);
+ }
+ }
+ } else {
+ Log.e(TAG, originalAction + " is not supported.");
+ return null;
+ }
final ResolveInfo ri = getPackageManager().resolveActivity(
resolveIntent, PackageManager.GET_META_DATA);
if (ri == null || ri.activityInfo == null) {
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index 1ec5325..4f74ca7 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -86,7 +86,6 @@
private final ChooserActivityLogger mChooserActivityLogger;
private int mNumShortcutResults = 0;
- private Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>();
private boolean mApplySharingAppLimits;
// Reserve spots for incoming direct share targets by adding placeholders
@@ -265,31 +264,20 @@
return;
}
- if (!(info instanceof DisplayResolveInfo)) {
- holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
- holder.bindIcon(info);
-
- if (info instanceof SelectableTargetInfo) {
- // direct share targets should append the application name for a better readout
- DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo();
- CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
- CharSequence extendedInfo = info.getExtendedInfo();
- String contentDescription = String.join(" ", info.getDisplayLabel(),
- extendedInfo != null ? extendedInfo : "", appName);
- holder.updateContentDescription(contentDescription);
- }
- } else {
+ holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
+ holder.bindIcon(info);
+ if (info instanceof SelectableTargetInfo) {
+ // direct share targets should append the application name for a better readout
+ DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo();
+ CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
+ CharSequence extendedInfo = info.getExtendedInfo();
+ String contentDescription = String.join(" ", info.getDisplayLabel(),
+ extendedInfo != null ? extendedInfo : "", appName);
+ holder.updateContentDescription(contentDescription);
+ } else if (info instanceof DisplayResolveInfo) {
DisplayResolveInfo dri = (DisplayResolveInfo) info;
- holder.bindLabel(dri.getDisplayLabel(), dri.getExtendedInfo(), alwaysShowSubLabel());
- LoadIconTask task = mIconLoaders.get(dri);
- if (task == null) {
- task = new LoadIconTask(dri, holder);
- mIconLoaders.put(dri, task);
- task.execute();
- } else {
- // The holder was potentially changed as the underlying items were
- // reshuffled, so reset the target holder
- task.setViewHolder(holder);
+ if (!dri.hasDisplayIcon()) {
+ loadIcon(dri);
}
}
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 0e1ed7b..c70e26f 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -55,6 +55,7 @@
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Insets;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -1475,14 +1476,21 @@
mMultiProfilePagerAdapter.getActiveListAdapter().mDisplayList.get(0);
boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK;
- ResolverListAdapter inactiveAdapter = mMultiProfilePagerAdapter.getInactiveListAdapter();
- DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0);
+ final ResolverListAdapter inactiveAdapter =
+ mMultiProfilePagerAdapter.getInactiveListAdapter();
+ final DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0);
// Load the icon asynchronously
ImageView icon = findViewById(R.id.icon);
- ResolverListAdapter.LoadIconTask iconTask = inactiveAdapter.new LoadIconTask(
- otherProfileResolveInfo, new ResolverListAdapter.ViewHolder(icon));
- iconTask.execute();
+ inactiveAdapter.new LoadIconTask(otherProfileResolveInfo) {
+ @Override
+ protected void onPostExecute(Drawable drawable) {
+ if (!isDestroyed()) {
+ otherProfileResolveInfo.setDisplayIcon(drawable);
+ new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo);
+ }
+ }
+ }.execute();
((TextView) findViewById(R.id.open_cross_profile)).setText(
getResources().getString(
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 66fff5c..f6075b0 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -58,7 +58,10 @@
import com.android.internal.app.chooser.TargetInfo;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
public class ResolverListAdapter extends BaseAdapter {
private static final String TAG = "ResolverListAdapter";
@@ -87,6 +90,8 @@
private Runnable mPostListReadyRunnable;
private final boolean mIsAudioCaptureDevice;
private boolean mIsTabLoaded;
+ private final Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>();
+ private final Map<DisplayResolveInfo, LoadLabelTask> mLabelLoaders = new HashMap<>();
public ResolverListAdapter(Context context, List<Intent> payloadIntents,
Intent[] initialIntents, List<ResolveInfo> rList,
@@ -636,26 +641,47 @@
if (info == null) {
holder.icon.setImageDrawable(
mContext.getDrawable(R.drawable.resolver_icon_placeholder));
+ holder.bindLabel("", "", false);
return;
}
- if (info instanceof DisplayResolveInfo
- && !((DisplayResolveInfo) info).hasDisplayLabel()) {
- getLoadLabelTask((DisplayResolveInfo) info, holder).execute();
- } else {
- holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
- }
-
- if (info instanceof DisplayResolveInfo
- && !((DisplayResolveInfo) info).hasDisplayIcon()) {
- new LoadIconTask((DisplayResolveInfo) info, holder).execute();
- } else {
+ if (info instanceof DisplayResolveInfo) {
+ DisplayResolveInfo dri = (DisplayResolveInfo) info;
+ boolean hasLabel = dri.hasDisplayLabel();
+ holder.bindLabel(
+ dri.getDisplayLabel(),
+ dri.getExtendedInfo(),
+ hasLabel && alwaysShowSubLabel());
holder.bindIcon(info);
+ if (!hasLabel) {
+ loadLabel(dri);
+ }
+ if (!dri.hasDisplayIcon()) {
+ loadIcon(dri);
+ }
}
}
- protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
- return new LoadLabelTask(info, holder);
+ protected final void loadIcon(DisplayResolveInfo info) {
+ LoadIconTask task = mIconLoaders.get(info);
+ if (task == null) {
+ task = new LoadIconTask((DisplayResolveInfo) info);
+ mIconLoaders.put(info, task);
+ task.execute();
+ }
+ }
+
+ private void loadLabel(DisplayResolveInfo info) {
+ LoadLabelTask task = mLabelLoaders.get(info);
+ if (task == null) {
+ task = createLoadLabelTask(info);
+ mLabelLoaders.put(info, task);
+ task.execute();
+ }
+ }
+
+ protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) {
+ return new LoadLabelTask(info);
}
public void onDestroy() {
@@ -666,6 +692,16 @@
if (mResolverListController != null) {
mResolverListController.destroy();
}
+ cancelTasks(mIconLoaders.values());
+ cancelTasks(mLabelLoaders.values());
+ mIconLoaders.clear();
+ mLabelLoaders.clear();
+ }
+
+ private <T extends AsyncTask> void cancelTasks(Collection<T> tasks) {
+ for (T task: tasks) {
+ task.cancel(false);
+ }
}
private static ColorMatrixColorFilter getSuspendedColorMatrix() {
@@ -883,11 +919,9 @@
protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
private final DisplayResolveInfo mDisplayResolveInfo;
- private final ViewHolder mHolder;
- protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) {
+ protected LoadLabelTask(DisplayResolveInfo dri) {
mDisplayResolveInfo = dri;
- mHolder = holder;
}
@Override
@@ -925,21 +959,22 @@
@Override
protected void onPostExecute(CharSequence[] result) {
+ if (mDisplayResolveInfo.hasDisplayLabel()) {
+ return;
+ }
mDisplayResolveInfo.setDisplayLabel(result[0]);
mDisplayResolveInfo.setExtendedInfo(result[1]);
- mHolder.bindLabel(result[0], result[1], alwaysShowSubLabel());
+ notifyDataSetChanged();
}
}
class LoadIconTask extends AsyncTask<Void, Void, Drawable> {
protected final DisplayResolveInfo mDisplayResolveInfo;
private final ResolveInfo mResolveInfo;
- private ViewHolder mHolder;
- LoadIconTask(DisplayResolveInfo dri, ViewHolder holder) {
+ LoadIconTask(DisplayResolveInfo dri) {
mDisplayResolveInfo = dri;
mResolveInfo = dri.getResolveInfo();
- mHolder = holder;
}
@Override
@@ -953,17 +988,9 @@
mResolverListCommunicator.updateProfileViewButton();
} else if (!mDisplayResolveInfo.hasDisplayIcon()) {
mDisplayResolveInfo.setDisplayIcon(d);
- mHolder.bindIcon(mDisplayResolveInfo);
- // Notify in case view is already bound to resolve the race conditions on
- // low end devices
notifyDataSetChanged();
}
}
-
- public void setViewHolder(ViewHolder holder) {
- mHolder = holder;
- mHolder.bindIcon(mDisplayResolveInfo);
- }
}
/**
diff --git a/core/proto/android/os/processstarttime.proto b/core/proto/android/os/processstarttime.proto
deleted file mode 100644
index d0f8bae..0000000
--- a/core/proto/android/os/processstarttime.proto
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2022 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.
- */
-
-syntax = "proto2";
-package android.os;
-
-option java_multiple_files = true;
-
-// This message is used for statsd logging and should be kept in sync with
-// frameworks/proto_logging/stats/atoms.proto
-/**
- * Logs information about process start time.
- *
- * Logged from:
- * frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
- */
-message ProcessStartTime {
- // The uid of the ProcessRecord.
- optional int32 uid = 1;
-
- // The process pid.
- optional int32 pid = 2;
-
- // The process name.
- // Usually package name, "system" for system server.
- // Provided by ActivityManagerService.
- optional string process_name = 3;
-
- enum StartType {
- UNKNOWN = 0;
- WARM = 1;
- HOT = 2;
- COLD = 3;
- }
-
- // The start type.
- optional StartType type = 4;
-
- // The elapsed realtime at the start of the process.
- optional int64 process_start_time_millis = 5;
-
- // Number of milliseconds it takes to reach bind application.
- optional int32 bind_application_delay_millis = 6;
-
- // Number of milliseconds it takes to finish start of the process.
- optional int32 process_start_delay_millis = 7;
-
- // hostingType field in ProcessRecord, the component type such as "activity",
- // "service", "content provider", "broadcast" or other strings.
- optional string hosting_type = 8;
-
- // hostingNameStr field in ProcessRecord. The component class name that runs
- // in this process.
- optional string hosting_name = 9;
-
- // Broadcast action name.
- optional string broadcast_action_name = 10;
-
- enum HostingTypeId {
- HOSTING_TYPE_UNKNOWN = 0;
- HOSTING_TYPE_ACTIVITY = 1;
- HOSTING_TYPE_ADDED_APPLICATION = 2;
- HOSTING_TYPE_BACKUP = 3;
- HOSTING_TYPE_BROADCAST = 4;
- HOSTING_TYPE_CONTENT_PROVIDER = 5;
- HOSTING_TYPE_LINK_FAIL = 6;
- HOSTING_TYPE_ON_HOLD = 7;
- HOSTING_TYPE_NEXT_ACTIVITY = 8;
- HOSTING_TYPE_NEXT_TOP_ACTIVITY = 9;
- HOSTING_TYPE_RESTART = 10;
- HOSTING_TYPE_SERVICE = 11;
- HOSTING_TYPE_SYSTEM = 12;
- HOSTING_TYPE_TOP_ACTIVITY = 13;
- HOSTING_TYPE_EMPTY = 14;
- }
-
- optional HostingTypeId hosting_type_id = 11;
-}
-
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
index 56a7070..2861428 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java
@@ -46,14 +46,14 @@
}
@Override
- protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
- return new LoadLabelWrapperTask(info, holder);
+ protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) {
+ return new LoadLabelWrapperTask(info);
}
class LoadLabelWrapperTask extends LoadLabelTask {
- protected LoadLabelWrapperTask(DisplayResolveInfo dri, ViewHolder holder) {
- super(dri, holder);
+ protected LoadLabelWrapperTask(DisplayResolveInfo dri) {
+ super(dri);
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.java
new file mode 100644
index 0000000..e029358
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DockStateReader.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 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.wm.shell.common;
+
+import static android.content.Intent.EXTRA_DOCK_STATE;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+import com.android.wm.shell.dagger.WMSingleton;
+
+import javax.inject.Inject;
+
+/**
+ * Provides information about the docked state of the device.
+ */
+@WMSingleton
+public class DockStateReader {
+
+ private static final IntentFilter DOCK_INTENT_FILTER = new IntentFilter(
+ Intent.ACTION_DOCK_EVENT);
+
+ private final Context mContext;
+
+ @Inject
+ public DockStateReader(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * @return True if the device is docked and false otherwise.
+ */
+ public boolean isDocked() {
+ Intent dockStatus = mContext.registerReceiver(/* receiver */ null, DOCK_INTENT_FILTER);
+ if (dockStatus != null) {
+ int dockState = dockStatus.getIntExtra(EXTRA_DOCK_STATE,
+ Intent.EXTRA_DOCK_STATE_UNDOCKED);
+ return dockState != Intent.EXTRA_DOCK_STATE_UNDOCKED;
+ }
+ return false;
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 235fd9c..6627de5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -37,6 +37,7 @@
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIWindowManager.CompatUIHintsState;
@@ -109,6 +110,7 @@
private final SyncTransactionQueue mSyncQueue;
private final ShellExecutor mMainExecutor;
private final Lazy<Transitions> mTransitionsLazy;
+ private final DockStateReader mDockStateReader;
private CompatUICallback mCallback;
@@ -127,7 +129,8 @@
DisplayImeController imeController,
SyncTransactionQueue syncQueue,
ShellExecutor mainExecutor,
- Lazy<Transitions> transitionsLazy) {
+ Lazy<Transitions> transitionsLazy,
+ DockStateReader dockStateReader) {
mContext = context;
mShellController = shellController;
mDisplayController = displayController;
@@ -138,6 +141,7 @@
mTransitionsLazy = transitionsLazy;
mCompatUIHintsState = new CompatUIHintsState();
shellInit.addInitCallback(this::onInit, this);
+ mDockStateReader = dockStateReader;
}
private void onInit() {
@@ -315,7 +319,8 @@
return new LetterboxEduWindowManager(context, taskInfo,
mSyncQueue, taskListener, mDisplayController.getDisplayLayout(taskInfo.displayId),
mTransitionsLazy.get(),
- this::onLetterboxEduDismissed);
+ this::onLetterboxEduDismissed,
+ mDockStateReader);
}
private void onLetterboxEduDismissed() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
index 35f1038..867d0ef 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java
@@ -34,6 +34,7 @@
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIWindowManagerAbstract;
import com.android.wm.shell.transition.Transitions;
@@ -88,19 +89,21 @@
*/
private final int mDialogVerticalMargin;
+ private final DockStateReader mDockStateReader;
+
public LetterboxEduWindowManager(Context context, TaskInfo taskInfo,
SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
DisplayLayout displayLayout, Transitions transitions,
- Runnable onDismissCallback) {
+ Runnable onDismissCallback, DockStateReader dockStateReader) {
this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions,
- onDismissCallback, new LetterboxEduAnimationController(context));
+ onDismissCallback, new LetterboxEduAnimationController(context), dockStateReader);
}
@VisibleForTesting
LetterboxEduWindowManager(Context context, TaskInfo taskInfo,
SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
DisplayLayout displayLayout, Transitions transitions, Runnable onDismissCallback,
- LetterboxEduAnimationController animationController) {
+ LetterboxEduAnimationController animationController, DockStateReader dockStateReader) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
mTransitions = transitions;
mOnDismissCallback = onDismissCallback;
@@ -111,6 +114,7 @@
Context.MODE_PRIVATE);
mDialogVerticalMargin = (int) mContext.getResources().getDimension(
R.dimen.letterbox_education_dialog_margin);
+ mDockStateReader = dockStateReader;
}
@Override
@@ -130,13 +134,15 @@
@Override
protected boolean eligibleToShowLayout() {
+ // - The letterbox education should not be visible if the device is docked.
// - If taskbar education is showing, the letterbox education shouldn't be shown for the
// given task until the taskbar education is dismissed and the compat info changes (then
// the controller will create a new instance of this class since this one isn't eligible).
// - If the layout isn't null then it was previously showing, and we shouldn't check if the
// user has seen the letterbox education before.
- return mEligibleForLetterboxEducation && !isTaskbarEduShowing() && (mLayout != null
- || !getHasSeenLetterboxEducation());
+ return mEligibleForLetterboxEducation && !isTaskbarEduShowing()
+ && (mLayout != null || !getHasSeenLetterboxEducation())
+ && !mDockStateReader.isDocked();
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index c25bbbf..7c9cabd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -46,6 +46,7 @@
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
@@ -69,7 +70,6 @@
import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.fullscreen.FullscreenTaskListener;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
-import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.Pip;
@@ -192,33 +192,16 @@
@WMSingleton
@Provides
- static KidsModeTaskOrganizer provideKidsModeTaskOrganizer(
- Context context,
- ShellInit shellInit,
- ShellCommandHandler shellCommandHandler,
- SyncTransactionQueue syncTransactionQueue,
- DisplayController displayController,
- DisplayInsetsController displayInsetsController,
- Optional<UnfoldAnimationController> unfoldAnimationController,
- Optional<RecentTasksController> recentTasksOptional,
- @ShellMainThread ShellExecutor mainExecutor,
- @ShellMainThread Handler mainHandler
- ) {
- return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler,
- syncTransactionQueue, displayController, displayInsetsController,
- unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler);
- }
-
- @WMSingleton
- @Provides
static CompatUIController provideCompatUIController(Context context,
ShellInit shellInit,
ShellController shellController,
DisplayController displayController, DisplayInsetsController displayInsetsController,
DisplayImeController imeController, SyncTransactionQueue syncQueue,
- @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy) {
+ @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy,
+ DockStateReader dockStateReader) {
return new CompatUIController(context, shellInit, shellController, displayController,
- displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy);
+ displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy,
+ dockStateReader);
}
@WMSingleton
@@ -781,7 +764,6 @@
DisplayInsetsController displayInsetsController,
DragAndDropController dragAndDropController,
ShellTaskOrganizer shellTaskOrganizer,
- KidsModeTaskOrganizer kidsModeTaskOrganizer,
Optional<BubbleController> bubblesOptional,
Optional<SplitScreenController> splitScreenOptional,
Optional<Pip> pipOptional,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 37a50b6..47b6659 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -56,6 +56,7 @@
import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
import com.android.wm.shell.freeform.FreeformTaskTransitionObserver;
import com.android.wm.shell.fullscreen.FullscreenTaskListener;
+import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.pip.PipAnimationController;
@@ -620,6 +621,28 @@
}
//
+ // Kids mode
+ //
+ @WMSingleton
+ @Provides
+ static KidsModeTaskOrganizer provideKidsModeTaskOrganizer(
+ Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ SyncTransactionQueue syncTransactionQueue,
+ DisplayController displayController,
+ DisplayInsetsController displayInsetsController,
+ Optional<UnfoldAnimationController> unfoldAnimationController,
+ Optional<RecentTasksController> recentTasksOptional,
+ @ShellMainThread ShellExecutor mainExecutor,
+ @ShellMainThread Handler mainHandler
+ ) {
+ return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler,
+ syncTransactionQueue, displayController, displayInsetsController,
+ unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler);
+ }
+
+ //
// Misc
//
@@ -630,6 +653,7 @@
@Provides
static Object provideIndependentShellComponentsToCreate(
DefaultMixedHandler defaultMixedHandler,
+ KidsModeTaskOrganizer kidsModeTaskOrganizer,
Optional<DesktopModeController> desktopModeController) {
return new Object();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
index 4def15d..2624ee5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl
@@ -59,10 +59,15 @@
/**
* Sets listener to get pinned stack animation callbacks.
*/
- oneway void setPinnedStackAnimationListener(IPipAnimationListener listener) = 3;
+ oneway void setPipAnimationListener(IPipAnimationListener listener) = 3;
/**
* Sets the shelf height and visibility.
*/
oneway void setShelfHeight(boolean visible, int shelfHeight) = 4;
+
+ /**
+ * Sets the next pip animation type to be the alpha animation.
+ */
+ oneway void setPipAnimationTypeToAlpha() = 5;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
index c06881a..72b9dd3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -51,15 +51,6 @@
}
/**
- * Sets both shelf visibility and its height.
- *
- * @param visible visibility of shelf.
- * @param height to specify the height for shelf.
- */
- default void setShelfHeight(boolean visible, int height) {
- }
-
- /**
* Set the callback when {@link PipTaskOrganizer#isInPip()} state is changed.
*
* @param callback The callback accepts the result of {@link PipTaskOrganizer#isInPip()}
@@ -68,14 +59,6 @@
default void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {}
/**
- * Set the pinned stack with {@link PipAnimationController.AnimationType}
- *
- * @param animationType The pre-defined {@link PipAnimationController.AnimationType}
- */
- default void setPinnedStackAnimationType(int animationType) {
- }
-
- /**
* Called when showing Pip menu.
*/
default void showPictureInPictureMenu() {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index af47666..3345b1b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -23,6 +23,7 @@
import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN;
@@ -1065,13 +1066,6 @@
}
@Override
- public void setShelfHeight(boolean visible, int height) {
- mMainExecutor.execute(() -> {
- PipController.this.setShelfHeight(visible, height);
- });
- }
-
- @Override
public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {
mMainExecutor.execute(() -> {
PipController.this.setOnIsInPipStateChangedListener(callback);
@@ -1079,13 +1073,6 @@
}
@Override
- public void setPinnedStackAnimationType(int animationType) {
- mMainExecutor.execute(() -> {
- PipController.this.setPinnedStackAnimationType(animationType);
- });
- }
-
- @Override
public void addPipExclusionBoundsChangeListener(Consumer<Rect> listener) {
mMainExecutor.execute(() -> {
mPipBoundsState.addPipExclusionBoundsChangeCallback(listener);
@@ -1178,8 +1165,8 @@
}
@Override
- public void setPinnedStackAnimationListener(IPipAnimationListener listener) {
- executeRemoteCallWithTaskPermission(mController, "setPinnedStackAnimationListener",
+ public void setPipAnimationListener(IPipAnimationListener listener) {
+ executeRemoteCallWithTaskPermission(mController, "setPipAnimationListener",
(controller) -> {
if (listener != null) {
mListener.register(listener);
@@ -1188,5 +1175,13 @@
}
});
}
+
+ @Override
+ public void setPipAnimationTypeToAlpha() {
+ executeRemoteCallWithTaskPermission(mController, "setPipAnimationTypeToAlpha",
+ (controller) -> {
+ controller.setPinnedStackAnimationType(ANIM_TYPE_ALPHA);
+ });
+ }
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 6292130..2fc0914 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -51,6 +51,7 @@
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.letterboxedu.LetterboxEduWindowManager;
@@ -93,6 +94,7 @@
private @Mock Lazy<Transitions> mMockTransitionsLazy;
private @Mock CompatUIWindowManager mMockCompatLayout;
private @Mock LetterboxEduWindowManager mMockLetterboxEduLayout;
+ private @Mock DockStateReader mDockStateReader;
@Captor
ArgumentCaptor<OnInsetsChangedListener> mOnInsetsChangedListenerCaptor;
@@ -113,7 +115,7 @@
mShellInit = spy(new ShellInit(mMockExecutor));
mController = new CompatUIController(mContext, mShellInit, mMockShellController,
mMockDisplayController, mMockDisplayInsetsController, mMockImeController,
- mMockSyncQueue, mMockExecutor, mMockTransitionsLazy) {
+ mMockSyncQueue, mMockExecutor, mMockTransitionsLazy, mDockStateReader) {
@Override
CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo,
ShellTaskOrganizer.TaskListener taskListener) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
index f3a8cf4..16517c0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java
@@ -54,6 +54,7 @@
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.transition.Transitions;
@@ -103,6 +104,7 @@
@Mock private SurfaceControlViewHost mViewHost;
@Mock private Transitions mTransitions;
@Mock private Runnable mOnDismissCallback;
+ @Mock private DockStateReader mDockStateReader;
private SharedPreferences mSharedPreferences;
@Nullable
@@ -153,6 +155,16 @@
}
@Test
+ public void testCreateLayout_eligibleAndDocked_doesNotCreateLayout() {
+ LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */
+ true, /* isDocked */ true);
+
+ assertFalse(windowManager.createLayout(/* canShow= */ true));
+
+ assertNull(windowManager.mLayout);
+ }
+
+ @Test
public void testCreateLayout_taskBarEducationIsShowing_doesNotCreateLayout() {
LetterboxEduWindowManager windowManager = createWindowManager(/* eligible= */
true, USER_ID_1, /* isTaskbarEduShowing= */ true);
@@ -382,17 +394,27 @@
return createWindowManager(eligible, USER_ID_1, /* isTaskbarEduShowing= */ false);
}
+ private LetterboxEduWindowManager createWindowManager(boolean eligible, boolean isDocked) {
+ return createWindowManager(eligible, USER_ID_1, /* isTaskbarEduShowing= */
+ false, isDocked);
+ }
+
private LetterboxEduWindowManager createWindowManager(boolean eligible,
int userId, boolean isTaskbarEduShowing) {
+ return createWindowManager(eligible, userId, isTaskbarEduShowing, /* isDocked */false);
+ }
+
+ private LetterboxEduWindowManager createWindowManager(boolean eligible,
+ int userId, boolean isTaskbarEduShowing, boolean isDocked) {
+ doReturn(isDocked).when(mDockStateReader).isDocked();
LetterboxEduWindowManager windowManager = new LetterboxEduWindowManager(mContext,
createTaskInfo(eligible, userId), mSyncTransactionQueue, mTaskListener,
createDisplayLayout(), mTransitions, mOnDismissCallback,
- mAnimationController);
+ mAnimationController, mDockStateReader);
spyOn(windowManager);
doReturn(mViewHost).when(windowManager).createSurfaceViewHost();
doReturn(isTaskbarEduShowing).when(windowManager).isTaskbarEduShowing();
-
return windowManager;
}
diff --git a/packages/SystemUI/res-keyguard/values/ids.xml b/packages/SystemUI/res-keyguard/values/ids.xml
new file mode 100644
index 0000000..0dff4ff
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/values/ids.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2022 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.
+ ~
+ -->
+
+<resources>
+ <item type="id" name="header_footer_views_added_tag_key" />
+</resources>
diff --git a/packages/SystemUI/res/layout/media_ttt_chip.xml b/packages/SystemUI/res/layout/media_ttt_chip.xml
index d886806..ae8e38e 100644
--- a/packages/SystemUI/res/layout/media_ttt_chip.xml
+++ b/packages/SystemUI/res/layout/media_ttt_chip.xml
@@ -16,7 +16,7 @@
<!-- Wrap in a frame layout so that we can update the margins on the inner layout. (Since this view
is the root view of a window, we cannot change the root view's margins.) -->
<!-- Alphas start as 0 because the view will be animated in. -->
-<FrameLayout
+<com.android.systemui.media.taptotransfer.sender.MediaTttChipRootView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/media_ttt_sender_chip"
@@ -97,4 +97,4 @@
/>
</LinearLayout>
-</FrameLayout>
+</com.android.systemui.media.taptotransfer.sender.MediaTttChipRootView>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 53f1227..9b9111f 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -246,6 +246,16 @@
<string name="screenrecord_start_label">Start Recording?</string>
<!-- Message reminding the user that sensitive information may be captured during a screen recording [CHAR_LIMIT=NONE]-->
<string name="screenrecord_description">While recording, Android System can capture any sensitive information that\u2019s visible on your screen or played on your device. This includes passwords, payment info, photos, messages, and audio.</string>
+ <!-- Dropdown option to record the entire screen [CHAR_LIMIT=30]-->
+ <string name="screenrecord_option_entire_screen">Record entire screen</string>
+ <!-- Dropdown option to record a single app [CHAR_LIMIT=30]-->
+ <string name="screenrecord_option_single_app">Record a single app</string>
+ <!-- Message reminding the user that sensitive information may be captured during a full screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]-->
+ <string name="screenrecord_warning_entire_screen">While you\'re recording, Android has access to anything visible on your screen or played on your device. So be careful with passwords, payment details, messages, or other sensitive information.</string>
+ <!-- Message reminding the user that sensitive information may be captured during a single app screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]-->
+ <string name="screenrecord_warning_single_app">While you\'re recording an app, Android has access to anything shown or played on that app. So be careful with passwords, payment details, messages, or other sensitive information.</string>
+ <!-- Button to start a screen recording in the updated screen record dialog that allows to select an app to record [CHAR LIMIT=50]-->
+ <string name="screenrecord_start_recording">Start recording</string>
<!-- Label for a switch to enable recording audio [CHAR LIMIT=NONE]-->
<string name="screenrecord_audio_label">Record audio</string>
<!-- Label for the option to record audio from the device [CHAR LIMIT=NONE]-->
@@ -958,7 +968,26 @@
<!-- Media projection permission dialog warning title. [CHAR LIMIT=NONE] -->
<string name="media_projection_dialog_title">Start recording or casting with <xliff:g id="app_seeking_permission" example="Hangouts">%s</xliff:g>?</string>
- <!-- Media projection permission dialog permanent grant check box. [CHAR LIMIT=NONE] -->
+ <!-- Media projection permission dialog title. [CHAR LIMIT=NONE] -->
+ <string name="media_projection_permission_dialog_title">Allow <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> to share or record?</string>
+
+ <!-- Media projection permission dropdown option for capturing the whole screen. [CHAR LIMIT=30] -->
+ <string name="media_projection_permission_dialog_option_entire_screen">Entire screen</string>
+
+ <!-- Media projection permission dropdown option for capturing single app. [CHAR LIMIT=30] -->
+ <string name="media_projection_permission_dialog_option_single_app">A single app</string>
+
+ <!-- Media projection permission warning for capturing the whole screen. [CHAR LIMIT=350] -->
+ <string name="media_projection_permission_dialog_warning_entire_screen">When you\'re sharing, recording, or casting, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything visible on your screen or played on your device. So be careful with passwords, payment details, messages, or other sensitive information.</string>
+
+ <!-- Media projection permission warning for capturing an app. [CHAR LIMIT=350] -->
+ <string name="media_projection_permission_dialog_warning_single_app">When you\'re sharing, recording, or casting an app, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything shown or played on that app. So be careful with passwords, payment details, messages, or other sensitive information.</string>
+
+ <!-- Media projection permission button to continue with app selection or recording [CHAR LIMIT=60] -->
+ <string name="media_projection_permission_dialog_continue">Continue</string>
+
+ <!-- Title of the dialog that allows to select an app to share or record [CHAR LIMIT=NONE] -->
+ <string name="media_projection_permission_app_selector_title">Share or record an app</string>
<!-- The text to clear all notifications. [CHAR LIMIT=60] -->
<string name="clear_all_notifications_text">Clear all</string>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index e77c650..2b2b05ce 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -81,11 +81,6 @@
*/
void stopScreenPinning() = 17;
- /*
- * Notifies that the swipe-to-home (recents animation) is finished.
- */
- void notifySwipeToHomeFinished() = 23;
-
/**
* Notifies that quickstep will switch to a new task
* @param rotation indicates which Surface.Rotation the gesture was started in
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
index efa5558..b793fd2 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java
@@ -66,10 +66,13 @@
listView.setDividerHeight(mContext.getResources().getDimensionPixelSize(
R.dimen.bouncer_user_switcher_popup_divider_height));
- int height = mContext.getResources().getDimensionPixelSize(
- R.dimen.bouncer_user_switcher_popup_header_height);
- listView.addHeaderView(createSpacer(height), null, false);
- listView.addFooterView(createSpacer(height), null, false);
+ if (listView.getTag(R.id.header_footer_views_added_tag_key) == null) {
+ int height = mContext.getResources().getDimensionPixelSize(
+ R.dimen.bouncer_user_switcher_popup_header_height);
+ listView.addHeaderView(createSpacer(height), null, false);
+ listView.addFooterView(createSpacer(height), null, false);
+ listView.setTag(R.id.header_footer_views_added_tag_key, new Object());
+ }
listView.setOnTouchListener((v, ev) -> {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SimpleMirrorWindowControl.java b/packages/SystemUI/src/com/android/systemui/accessibility/SimpleMirrorWindowControl.java
index 2ba2bb6..ed6fbec 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SimpleMirrorWindowControl.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SimpleMirrorWindowControl.java
@@ -45,7 +45,7 @@
private boolean mShouldSetTouchStart;
@Nullable private MoveWindowTask mMoveWindowTask;
- private PointF mLastDrag = new PointF();
+ private final PointF mLastDrag = new PointF();
private final Handler mHandler;
SimpleMirrorWindowControl(Context context, Handler handler) {
@@ -92,8 +92,7 @@
}
private Point findOffset(View v, int moveFrameAmount) {
- final Point offset = mTmpPoint;
- offset.set(0, 0);
+ mTmpPoint.set(0, 0);
if (v.getId() == R.id.left_control) {
mTmpPoint.x = -moveFrameAmount;
} else if (v.getId() == R.id.up_control) {
@@ -184,7 +183,7 @@
private final int mYOffset;
private final Handler mHandler;
/** Time in milliseconds between successive task executions.*/
- private long mPeriod;
+ private final long mPeriod;
private boolean mCancel;
MoveWindowTask(@NonNull MirrorWindowDelegate windowDelegate, Handler handler, int xOffset,
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index ae73e34..8ded268 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -94,7 +94,7 @@
private final Context mContext;
private final Resources mResources;
private final Handler mHandler;
- private Rect mWindowBounds;
+ private final Rect mWindowBounds;
private final int mDisplayId;
@Surface.Rotation
@VisibleForTesting
@@ -174,16 +174,16 @@
private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider;
private final MagnificationGestureDetector mGestureDetector;
private final int mBounceEffectDuration;
- private Choreographer.FrameCallback mMirrorViewGeometryVsyncCallback;
+ private final Choreographer.FrameCallback mMirrorViewGeometryVsyncCallback;
private Locale mLocale;
private NumberFormat mPercentFormat;
private float mBounceEffectAnimationScale;
- private SysUiState mSysUiState;
+ private final SysUiState mSysUiState;
// Set it to true when the view is overlapped with the gesture insets at the bottom.
private boolean mOverlapWithGestureInsets;
@Nullable
- private MirrorWindowControl mMirrorWindowControl;
+ private final MirrorWindowControl mMirrorWindowControl;
WindowMagnificationController(@UiContext Context context, @NonNull Handler handler,
@NonNull WindowMagnificationAnimationController animationController,
@@ -489,9 +489,7 @@
/** Returns the rotation degree change of two {@link Surface.Rotation} */
private int getDegreeFromRotation(@Surface.Rotation int newRotation,
@Surface.Rotation int oldRotation) {
- final int rotationDiff = oldRotation - newRotation;
- final int degree = (rotationDiff + 4) % 4 * 90;
- return degree;
+ return (oldRotation - newRotation + 4) % 4 * 90;
}
private void createMirrorWindow() {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 443d277..06dbab9 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -81,6 +81,7 @@
import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule;
import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule;
import com.android.systemui.statusbar.window.StatusBarWindowModule;
+import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule;
import com.android.systemui.tuner.dagger.TunerModule;
import com.android.systemui.unfold.SysUIUnfoldModule;
import com.android.systemui.user.UserModule;
@@ -145,6 +146,7 @@
StatusBarWindowModule.class,
SysUIConcurrencyModule.class,
SysUIUnfoldModule.class,
+ TelephonyRepositoryModule.class,
TunerModule.class,
UserModule.class,
UtilModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 93f13eb..43742a8 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -104,9 +104,26 @@
public static final ReleasedFlag MODERN_USER_SWITCHER_ACTIVITY =
new ReleasedFlag(209, true);
- /** Whether the new implementation of UserSwitcherController should be used. */
- public static final UnreleasedFlag REFACTORED_USER_SWITCHER_CONTROLLER =
- new UnreleasedFlag(210, false);
+ /**
+ * Whether the user interactor and repository should use `UserSwitcherController`.
+ *
+ * <p>If this is {@code false}, the interactor and repo skip the controller and directly access
+ * the framework APIs.
+ */
+ public static final UnreleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER =
+ new UnreleasedFlag(210, true);
+
+ /**
+ * Whether `UserSwitcherController` should use the user interactor.
+ *
+ * <p>When this is {@code true}, the controller does not directly access framework APIs.
+ * Instead, it goes through the interactor.
+ *
+ * <p>Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is
+ * {@code true} as it would created a cycle between controller -> interactor -> controller.
+ */
+ public static final UnreleasedFlag USER_CONTROLLER_USES_INTERACTOR =
+ new UnreleasedFlag(211, false);
/***************************************/
// 300 - power menu
@@ -247,6 +264,13 @@
public static final SysPropBooleanFlag SHOW_FLOATING_TASKS_AS_BUBBLES =
new SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false);
+ @Keep
+ public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_BUBBLE =
+ new SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true);
+ @Keep
+ public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_PIP =
+ new SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true);
+
// 1200 - predictive back
@Keep
public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 840a4b2..4c4b588 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -85,6 +85,15 @@
*/
val dozeAmount: Flow<Float>
+ /**
+ * Returns `true` if the keyguard is showing; `false` otherwise.
+ *
+ * Note: this is also `true` when the lock-screen is occluded with an `Activity` "above" it in
+ * the z-order (which is not really above the system UI window, but rather - the lock-screen
+ * becomes invisible to reveal the "occluding activity").
+ */
+ fun isKeyguardShowing(): Boolean
+
/** Sets whether the bottom area UI should animate the transition out of doze state. */
fun setAnimateDozingTransitions(animate: Boolean)
@@ -103,7 +112,7 @@
@Inject
constructor(
statusBarStateController: StatusBarStateController,
- keyguardStateController: KeyguardStateController,
+ private val keyguardStateController: KeyguardStateController,
dozeHost: DozeHost,
) : KeyguardRepository {
private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
@@ -168,6 +177,10 @@
awaitClose { statusBarStateController.removeCallback(callback) }
}
+ override fun isKeyguardShowing(): Boolean {
+ return keyguardStateController.isShowing
+ }
+
override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.value = animate
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index dccc941..192919e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -29,7 +29,7 @@
class KeyguardInteractor
@Inject
constructor(
- repository: KeyguardRepository,
+ private val repository: KeyguardRepository,
) {
/**
* The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at
@@ -40,4 +40,8 @@
val isDozing: Flow<Boolean> = repository.isDozing
/** Whether the keyguard is showing ot not. */
val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
+
+ fun isKeyguardShowing(): Boolean {
+ return repository.isKeyguardShowing()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index 4379d25..aae973d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
@@ -25,6 +25,7 @@
import com.android.internal.logging.UiEventLogger
import com.android.internal.statusbar.IUndoMediaTransferCallback
import com.android.systemui.R
+import com.android.systemui.plugins.FalsingManager
import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS
/**
@@ -107,12 +108,15 @@
controllerSender: MediaTttChipControllerSender,
routeInfo: MediaRoute2Info,
undoCallback: IUndoMediaTransferCallback?,
- uiEventLogger: MediaTttSenderUiEventLogger
+ uiEventLogger: MediaTttSenderUiEventLogger,
+ falsingManager: FalsingManager,
): View.OnClickListener? {
if (undoCallback == null) {
return null
}
return View.OnClickListener {
+ if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
+
uiEventLogger.logUndoClicked(
MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED
)
@@ -143,12 +147,15 @@
controllerSender: MediaTttChipControllerSender,
routeInfo: MediaRoute2Info,
undoCallback: IUndoMediaTransferCallback?,
- uiEventLogger: MediaTttSenderUiEventLogger
+ uiEventLogger: MediaTttSenderUiEventLogger,
+ falsingManager: FalsingManager,
): View.OnClickListener? {
if (undoCallback == null) {
return null
}
return View.OnClickListener {
+ if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener
+
uiEventLogger.logUndoClicked(
MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED
)
@@ -215,7 +222,8 @@
controllerSender: MediaTttChipControllerSender,
routeInfo: MediaRoute2Info,
undoCallback: IUndoMediaTransferCallback?,
- uiEventLogger: MediaTttSenderUiEventLogger
+ uiEventLogger: MediaTttSenderUiEventLogger,
+ falsingManager: FalsingManager,
): View.OnClickListener? = null
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
index e539f3f..007eb8f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
@@ -22,25 +22,30 @@
import android.os.PowerManager
import android.util.Log
import android.view.Gravity
+import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import android.widget.TextView
import com.android.internal.statusbar.IUndoMediaTransferCallback
+import com.android.systemui.Gefingerpoken
import com.android.systemui.R
import com.android.systemui.animation.Interpolators
import com.android.systemui.animation.ViewHierarchyAnimator
+import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.taptotransfer.common.MediaTttLogger
import com.android.systemui.media.taptotransfer.common.MediaTttUtils
+import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.temporarydisplay.TemporaryDisplayRemovalReason
import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
import com.android.systemui.temporarydisplay.TemporaryViewInfo
import com.android.systemui.util.concurrency.DelayableExecutor
+import dagger.Lazy
import javax.inject.Inject
/**
@@ -57,7 +62,11 @@
accessibilityManager: AccessibilityManager,
configurationController: ConfigurationController,
powerManager: PowerManager,
- private val uiEventLogger: MediaTttSenderUiEventLogger
+ private val uiEventLogger: MediaTttSenderUiEventLogger,
+ // Added Lazy<> to delay the time we create Falsing instances.
+ // And overcome performance issue, check [b/247817628] for details.
+ private val falsingManager: Lazy<FalsingManager>,
+ private val falsingCollector: Lazy<FalsingCollector>,
) : TemporaryViewDisplayController<ChipSenderInfo, MediaTttLogger>(
context,
logger,
@@ -70,6 +79,9 @@
MediaTttUtils.WINDOW_TITLE,
MediaTttUtils.WAKE_REASON,
) {
+
+ private lateinit var parent: MediaTttChipRootView
+
override val windowLayoutParams = commonWindowLayoutParams.apply {
gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL)
}
@@ -120,6 +132,15 @@
val chipState = newInfo.state
+ // Detect falsing touches on the chip.
+ parent = currentView.requireViewById(R.id.media_ttt_sender_chip)
+ parent.touchHandler = object : Gefingerpoken {
+ override fun onTouchEvent(ev: MotionEvent?): Boolean {
+ falsingCollector.get().onTouchEvent(ev)
+ return false
+ }
+ }
+
// App icon
val iconInfo = MediaTttUtils.getIconInfoFromPackageName(
context, newInfo.routeInfo.clientPackageName, logger
@@ -142,7 +163,11 @@
// Undo
val undoView = currentView.requireViewById<View>(R.id.undo)
val undoClickListener = chipState.undoClickListener(
- this, newInfo.routeInfo, newInfo.undoCallback, uiEventLogger
+ this,
+ newInfo.routeInfo,
+ newInfo.undoCallback,
+ uiEventLogger,
+ falsingManager.get(),
)
undoView.setOnClickListener(undoClickListener)
undoView.visibility = (undoClickListener != null).visibleIfTrue()
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt
new file mode 100644
index 0000000..3373159
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2022 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.media.taptotransfer.sender
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.widget.FrameLayout
+import com.android.systemui.Gefingerpoken
+
+/** A simple subclass that allows for observing touch events on chip. */
+class MediaTttChipRootView(
+ context: Context,
+ attrs: AttributeSet?
+) : FrameLayout(context, attrs) {
+
+ /** Assign this field to observe touch events. */
+ var touchHandler: Gefingerpoken? = null
+
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
+ touchHandler?.onTouchEvent(ev)
+ return super.dispatchTouchEvent(ev)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 0f1338e..b26b42c 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -57,7 +57,6 @@
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.policy.GestureNavigationSettingsObserver;
-import com.android.internal.util.LatencyTracker;
import com.android.systemui.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.qualifiers.Background;
@@ -199,7 +198,7 @@
private final Rect mNavBarOverlayExcludedBounds = new Rect();
private final Region mExcludeRegion = new Region();
private final Region mUnrestrictedExcludeRegion = new Region();
- private final LatencyTracker mLatencyTracker;
+ private final Provider<NavigationBarEdgePanel> mNavBarEdgePanelProvider;
private final Provider<BackGestureTfClassifierProvider>
mBackGestureTfClassifierProviderProvider;
private final FeatureFlags mFeatureFlags;
@@ -339,7 +338,7 @@
IWindowManager windowManagerService,
Optional<Pip> pipOptional,
FalsingManager falsingManager,
- LatencyTracker latencyTracker,
+ Provider<NavigationBarEdgePanel> navigationBarEdgePanelProvider,
Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider,
FeatureFlags featureFlags) {
super(broadcastDispatcher);
@@ -358,7 +357,7 @@
mWindowManagerService = windowManagerService;
mPipOptional = pipOptional;
mFalsingManager = falsingManager;
- mLatencyTracker = latencyTracker;
+ mNavBarEdgePanelProvider = navigationBarEdgePanelProvider;
mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider;
mFeatureFlags = featureFlags;
mLastReportedConfig.setTo(mContext.getResources().getConfiguration());
@@ -583,8 +582,7 @@
setEdgeBackPlugin(
mBackPanelControllerFactory.create(mContext));
} else {
- setEdgeBackPlugin(
- new NavigationBarEdgePanel(mContext, mLatencyTracker));
+ setEdgeBackPlugin(mNavBarEdgePanelProvider.get());
}
}
@@ -1091,7 +1089,7 @@
private final IWindowManager mWindowManagerService;
private final Optional<Pip> mPipOptional;
private final FalsingManager mFalsingManager;
- private final LatencyTracker mLatencyTracker;
+ private final Provider<NavigationBarEdgePanel> mNavBarEdgePanelProvider;
private final Provider<BackGestureTfClassifierProvider>
mBackGestureTfClassifierProviderProvider;
private final FeatureFlags mFeatureFlags;
@@ -1111,7 +1109,7 @@
IWindowManager windowManagerService,
Optional<Pip> pipOptional,
FalsingManager falsingManager,
- LatencyTracker latencyTracker,
+ Provider<NavigationBarEdgePanel> navBarEdgePanelProvider,
Provider<BackGestureTfClassifierProvider>
backGestureTfClassifierProviderProvider,
FeatureFlags featureFlags) {
@@ -1129,7 +1127,7 @@
mWindowManagerService = windowManagerService;
mPipOptional = pipOptional;
mFalsingManager = falsingManager;
- mLatencyTracker = latencyTracker;
+ mNavBarEdgePanelProvider = navBarEdgePanelProvider;
mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider;
mFeatureFlags = featureFlags;
}
@@ -1152,7 +1150,7 @@
mWindowManagerService,
mPipOptional,
mFalsingManager,
- mLatencyTracker,
+ mNavBarEdgePanelProvider,
mBackGestureTfClassifierProviderProvider,
mFeatureFlags);
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
index 24efc76..1230708 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java
@@ -52,9 +52,9 @@
import com.android.internal.util.LatencyTracker;
import com.android.settingslib.Utils;
-import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.plugins.NavigationEdgeBackPlugin;
import com.android.systemui.shared.navigationbar.RegionSamplingHelper;
import com.android.systemui.statusbar.VibratorHelper;
@@ -62,6 +62,8 @@
import java.io.PrintWriter;
import java.util.concurrent.Executor;
+import javax.inject.Inject;
+
public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPlugin {
private static final String TAG = "NavigationBarEdgePanel";
@@ -282,11 +284,16 @@
};
private BackCallback mBackCallback;
- public NavigationBarEdgePanel(Context context, LatencyTracker latencyTracker) {
+ @Inject
+ public NavigationBarEdgePanel(
+ Context context,
+ LatencyTracker latencyTracker,
+ VibratorHelper vibratorHelper,
+ @Background Executor backgroundExecutor) {
super(context);
mWindowManager = context.getSystemService(WindowManager.class);
- mVibratorHelper = Dependency.get(VibratorHelper.class);
+ mVibratorHelper = vibratorHelper;
mDensity = context.getResources().getDisplayMetrics().density;
@@ -358,7 +365,6 @@
setVisibility(GONE);
- Executor backgroundExecutor = Dependency.get(Dependency.BACKGROUND_EXECUTOR);
boolean isPrimaryDisplay = mContext.getDisplayId() == DEFAULT_DISPLAY;
mRegionSamplingHelper = new RegionSamplingHelper(this,
new RegionSamplingHelper.SamplingCallback() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 7b27cf4..1ef6426 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -60,6 +60,7 @@
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
@@ -82,7 +83,7 @@
private static final String EXTRA_VISIBLE = "visible";
private final Rect mQsBounds = new Rect();
- private final StatusBarStateController mStatusBarStateController;
+ private final SysuiStatusBarStateController mStatusBarStateController;
private final FalsingManager mFalsingManager;
private final KeyguardBypassController mBypassController;
private boolean mQsExpanded;
@@ -159,7 +160,7 @@
* Progress of pull down from the center of the lock screen.
* @see com.android.systemui.statusbar.LockscreenShadeTransitionController
*/
- private float mFullShadeProgress;
+ private float mLockscreenToShadeProgress;
private boolean mOverScrolling;
@@ -177,7 +178,7 @@
@Inject
public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
QSTileHost qsTileHost,
- StatusBarStateController statusBarStateController, CommandQueue commandQueue,
+ SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue,
@Named(QS_PANEL) MediaHost qsMediaHost,
@Named(QUICK_QS_PANEL) MediaHost qqsMediaHost,
KeyguardBypassController keyguardBypassController,
@@ -442,20 +443,19 @@
}
private void updateQsState() {
- final boolean expanded = mQsExpanded || mInSplitShade;
- final boolean expandVisually = expanded || mStackScrollerOverscrolling
+ final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling
|| mHeaderAnimating;
- mQSPanelController.setExpanded(expanded);
+ mQSPanelController.setExpanded(mQsExpanded);
boolean keyguardShowing = isKeyguardState();
- mHeader.setVisibility((expanded || !keyguardShowing || mHeaderAnimating
+ mHeader.setVisibility((mQsExpanded || !keyguardShowing || mHeaderAnimating
|| mShowCollapsedOnKeyguard)
? View.VISIBLE
: View.INVISIBLE);
mHeader.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
- || (expanded && !mStackScrollerOverscrolling), mQuickQSPanelController);
+ || (mQsExpanded && !mStackScrollerOverscrolling), mQuickQSPanelController);
boolean qsPanelVisible = !mQsDisabled && expandVisually;
- boolean footerVisible = qsPanelVisible && (expanded || !keyguardShowing || mHeaderAnimating
- || mShowCollapsedOnKeyguard);
+ boolean footerVisible = qsPanelVisible && (mQsExpanded || !keyguardShowing
+ || mHeaderAnimating || mShowCollapsedOnKeyguard);
mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
if (mQSFooterActionController != null) {
mQSFooterActionController.setVisible(footerVisible);
@@ -463,7 +463,7 @@
mQSFooterActionsViewModel.onVisibilityChangeRequested(footerVisible);
}
mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
- || (expanded && !mStackScrollerOverscrolling));
+ || (mQsExpanded && !mStackScrollerOverscrolling));
mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE);
if (DEBUG) {
Log.d(TAG, "Footer: " + footerVisible + ", QS Panel: " + qsPanelVisible);
@@ -586,7 +586,7 @@
mTransitioningToFullShade = isTransitioningToFullShade;
updateShowCollapsedOnKeyguard();
}
- mFullShadeProgress = qsTransitionFraction;
+ mLockscreenToShadeProgress = qsTransitionFraction;
setQsExpansion(mLastQSExpansion, mLastPanelFraction, mLastHeaderTranslation,
isTransitioningToFullShade ? qsSquishinessFraction : mSquishinessFraction);
}
@@ -710,10 +710,13 @@
}
if (mInSplitShade) {
// Large screens in landscape.
- if (mTransitioningToFullShade || isKeyguardState()) {
+ // Need to check upcoming state as for unlocked -> AOD transition current state is
+ // not updated yet, but we're transitioning and UI should already follow KEYGUARD state
+ if (mTransitioningToFullShade || mStatusBarStateController.getCurrentOrUpcomingState()
+ == StatusBarState.KEYGUARD) {
// Always use "mFullShadeProgress" on keyguard, because
// "panelExpansionFractions" is always 1 on keyguard split shade.
- return mFullShadeProgress;
+ return mLockscreenToShadeProgress;
} else {
return panelExpansionFraction;
}
@@ -722,7 +725,7 @@
if (mTransitioningToFullShade) {
// Only use this value during the standard lock screen shade expansion. During the
// "quick" expansion from top, this value is 0.
- return mFullShadeProgress;
+ return mLockscreenToShadeProgress;
} else {
return panelExpansionFraction;
}
@@ -930,7 +933,7 @@
indentingPw.println("mLastHeaderTranslation: " + mLastHeaderTranslation);
indentingPw.println("mInSplitShade: " + mInSplitShade);
indentingPw.println("mTransitioningToFullShade: " + mTransitioningToFullShade);
- indentingPw.println("mFullShadeProgress: " + mFullShadeProgress);
+ indentingPw.println("mLockscreenToShadeProgress: " + mLockscreenToShadeProgress);
indentingPw.println("mOverScrolling: " + mOverScrolling);
indentingPw.println("isCustomizing: " + mQSCustomizerController.isCustomizing());
View view = getView();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index 97476b2..d2d5063 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -134,7 +134,7 @@
v.bind(name, drawable, item.info.id);
}
v.setActivated(item.isCurrent);
- v.setDisabledByAdmin(mController.isDisabledByAdmin(item));
+ v.setDisabledByAdmin(item.isDisabledByAdmin());
v.setEnabled(item.isSwitchToEnabled);
UserSwitcherController.setSelectableAlpha(v);
@@ -173,16 +173,16 @@
Trace.beginSection("UserDetailView.Adapter#onClick");
UserRecord userRecord =
(UserRecord) view.getTag();
- if (mController.isDisabledByAdmin(userRecord)) {
+ if (userRecord.isDisabledByAdmin()) {
final Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
- mContext, mController.getEnforcedAdmin(userRecord));
+ mContext, userRecord.enforcedAdmin);
mController.startActivity(intent);
} else if (userRecord.isSwitchToEnabled) {
MetricsLogger.action(mContext, MetricsEvent.QS_SWITCH_USER);
mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH);
if (!userRecord.isAddUser
&& !userRecord.isRestricted
- && !mController.isDisabledByAdmin(userRecord)) {
+ && !userRecord.isDisabledByAdmin()) {
if (mCurrentUserView != null) {
mCurrentUserView.setActivated(false);
}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 7e2a5c5..899e57d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -342,14 +342,6 @@
}
@Override
- public void notifySwipeToHomeFinished() {
- verifyCallerAndClearCallingIdentity("notifySwipeToHomeFinished", () ->
- mPipOptional.ifPresent(
- pip -> pip.setPinnedStackAnimationType(
- PipAnimationController.ANIM_TYPE_ALPHA)));
- }
-
- @Override
public void notifySwipeUpGestureStarted() {
verifyCallerAndClearCallingIdentityPostMain("notifySwipeUpGestureStarted", () ->
notifySwipeUpGestureStartedInternal());
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
index 55602a9..e3658de 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java
@@ -19,6 +19,7 @@
import static android.os.FileUtils.closeQuietly;
import android.annotation.IntRange;
+import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.graphics.Bitmap;
@@ -29,6 +30,7 @@
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.os.Trace;
+import android.os.UserHandle;
import android.provider.MediaStore;
import android.util.Log;
@@ -142,8 +144,9 @@
*
* @return a listenable future result
*/
- ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap) {
- return export(executor, requestId, bitmap, ZonedDateTime.now());
+ ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
+ UserHandle owner) {
+ return export(executor, requestId, bitmap, ZonedDateTime.now(), owner);
}
/**
@@ -155,10 +158,10 @@
* @return a listenable future result
*/
ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap,
- ZonedDateTime captureTime) {
+ ZonedDateTime captureTime, UserHandle owner) {
final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat,
- mQuality, /* publish */ true);
+ mQuality, /* publish */ true, owner);
return CallbackToFutureAdapter.getFuture(
(completer) -> {
@@ -174,28 +177,6 @@
);
}
- /**
- * Delete the entry.
- *
- * @param executor the thread for execution
- * @param uri the uri of the image to publish
- *
- * @return a listenable future result
- */
- ListenableFuture<Result> delete(Executor executor, Uri uri) {
- return CallbackToFutureAdapter.getFuture((completer) -> {
- executor.execute(() -> {
- mResolver.delete(uri, null);
-
- Result result = new Result();
- result.uri = uri;
- result.deleted = true;
- completer.set(result);
- });
- return "ContentResolver#delete";
- });
- }
-
static class Result {
Uri uri;
UUID requestId;
@@ -203,7 +184,6 @@
long timestamp;
CompressFormat format;
boolean published;
- boolean deleted;
@Override
public String toString() {
@@ -214,7 +194,6 @@
sb.append(", timestamp=").append(timestamp);
sb.append(", format=").append(format);
sb.append(", published=").append(published);
- sb.append(", deleted=").append(deleted);
sb.append('}');
return sb.toString();
}
@@ -227,17 +206,19 @@
private final ZonedDateTime mCaptureTime;
private final CompressFormat mFormat;
private final int mQuality;
+ private final UserHandle mOwner;
private final String mFileName;
private final boolean mPublish;
Task(ContentResolver resolver, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime,
- CompressFormat format, int quality, boolean publish) {
+ CompressFormat format, int quality, boolean publish, UserHandle owner) {
mResolver = resolver;
mRequestId = requestId;
mBitmap = bitmap;
mCaptureTime = captureTime;
mFormat = format;
mQuality = quality;
+ mOwner = owner;
mFileName = createFilename(mCaptureTime, mFormat);
mPublish = publish;
}
@@ -253,7 +234,7 @@
start = Instant.now();
}
- uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName);
+ uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName, mOwner);
throwIfInterrupted();
writeImage(mResolver, mBitmap, mFormat, mQuality, uri);
@@ -297,15 +278,20 @@
}
private static Uri createEntry(ContentResolver resolver, CompressFormat format,
- ZonedDateTime time, String fileName) throws ImageExportException {
+ ZonedDateTime time, String fileName, UserHandle owner) throws ImageExportException {
Trace.beginSection("ImageExporter_createEntry");
try {
final ContentValues values = createMetadata(time, format, fileName);
- Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
+ Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ if (UserHandle.myUserId() != owner.getIdentifier()) {
+ baseUri = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier());
+ }
+ Uri uri = resolver.insert(baseUri, values);
if (uri == null) {
throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL);
}
+ Log.d(TAG, "Inserted new URI: " + uri);
return uri;
} finally {
Trace.endSection();
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
index ba6e98e..8bf956b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
@@ -30,6 +30,7 @@
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
@@ -387,7 +388,9 @@
mOutputBitmap = renderBitmap(drawable, bounds);
ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export(
- mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now());
+ mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now(),
+ // TODO: Owner must match the owner of the captured window.
+ Process.myUserHandle());
exportFuture.addListener(() -> onExportCompleted(action, exportFuture), mUiExecutor);
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index f248d69..077ad35 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -48,6 +48,8 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.systemui.R;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
import com.google.common.util.concurrent.ListenableFuture;
@@ -71,6 +73,7 @@
private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
private final Context mContext;
+ private FeatureFlags mFlags;
private final ScreenshotSmartActions mScreenshotSmartActions;
private final ScreenshotController.SaveImageInBackgroundData mParams;
private final ScreenshotController.SavedImageData mImageData;
@@ -84,7 +87,10 @@
private final ImageExporter mImageExporter;
private long mImageTime;
- SaveImageInBackgroundTask(Context context, ImageExporter exporter,
+ SaveImageInBackgroundTask(
+ Context context,
+ FeatureFlags flags,
+ ImageExporter exporter,
ScreenshotSmartActions screenshotSmartActions,
ScreenshotController.SaveImageInBackgroundData data,
Supplier<ActionTransition> sharedElementTransition,
@@ -92,6 +98,7 @@
screenshotNotificationSmartActionsProvider
) {
mContext = context;
+ mFlags = flags;
mScreenshotSmartActions = screenshotSmartActions;
mImageData = new ScreenshotController.SavedImageData();
mQuickShareData = new ScreenshotController.QuickShareData();
@@ -117,7 +124,8 @@
}
// TODO: move to constructor / from ScreenshotRequest
final UUID requestId = UUID.randomUUID();
- final UserHandle user = getUserHandleOfForegroundApplication(mContext);
+ final UserHandle user = mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY)
+ ? mParams.owner : getUserHandleOfForegroundApplication(mContext);
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
@@ -133,8 +141,9 @@
// Call synchronously here since already on a background thread.
ListenableFuture<ImageExporter.Result> future =
- mImageExporter.export(Runnable::run, requestId, image);
+ mImageExporter.export(Runnable::run, requestId, image, mParams.owner);
ImageExporter.Result result = future.get();
+ Log.d(TAG, "Saved screenshot: " + result);
final Uri uri = result.uri;
mImageTime = result.timestamp;
@@ -157,6 +166,7 @@
}
mImageData.uri = uri;
+ mImageData.owner = user;
mImageData.smartActions = smartActions;
mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri);
mImageData.editTransition = createEditAction(mContext, mContext.getResources(), uri);
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index 3fee232..df32d20 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -34,6 +34,7 @@
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.MainThread;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -57,7 +58,9 @@
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Process;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.provider.Settings;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -90,6 +93,7 @@
import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.clipboardoverlay.ClipboardOverlayController;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
import com.android.systemui.util.Assert;
@@ -151,6 +155,7 @@
public Consumer<Uri> finisher;
public ScreenshotController.ActionsReadyListener mActionsReadyListener;
public ScreenshotController.QuickShareActionReadyListener mQuickShareActionsReadyListener;
+ public UserHandle owner;
void clearImage() {
image = null;
@@ -167,6 +172,8 @@
public Notification.Action deleteAction;
public List<Notification.Action> smartActions;
public Notification.Action quickShareAction;
+ public UserHandle owner;
+
/**
* POD for shared element transition.
@@ -242,6 +249,7 @@
private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000;
private final WindowContext mContext;
+ private final FeatureFlags mFlags;
private final ScreenshotNotificationsController mNotificationsController;
private final ScreenshotSmartActions mScreenshotSmartActions;
private final UiEventLogger mUiEventLogger;
@@ -288,6 +296,7 @@
@Inject
ScreenshotController(
Context context,
+ FeatureFlags flags,
ScreenshotSmartActions screenshotSmartActions,
ScreenshotNotificationsController screenshotNotificationsController,
ScrollCaptureClient scrollCaptureClient,
@@ -331,6 +340,7 @@
final Context displayContext = context.createDisplayContext(getDefaultDisplay());
mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
mWindowManager = mContext.getSystemService(WindowManager.class);
+ mFlags = flags;
mAccessibilityManager = AccessibilityManager.getInstance(mContext);
@@ -377,7 +387,6 @@
void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds,
Insets visibleInsets, int taskId, int userId, ComponentName topComponent,
Consumer<Uri> finisher, RequestCallback requestCallback) {
- // TODO: use task Id, userId, topComponent for smart handler
Assert.isMainThread();
if (screenshot == null) {
Log.e(TAG, "Got null bitmap from screenshot message");
@@ -395,7 +404,7 @@
}
mCurrentRequestCallback = requestCallback;
saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, topComponent,
- showFlash);
+ showFlash, UserHandle.of(userId));
}
/**
@@ -543,14 +552,15 @@
return;
}
- saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true);
+ saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true,
+ Process.myUserHandle());
mBroadcastSender.sendBroadcast(new Intent(ClipboardOverlayController.SCREENSHOT_ACTION),
ClipboardOverlayController.SELF_PERMISSION);
}
private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect,
- Insets screenInsets, ComponentName topComponent, boolean showFlash) {
+ Insets screenInsets, ComponentName topComponent, boolean showFlash, UserHandle owner) {
withWindowAttached(() ->
mScreenshotView.announceForAccessibility(
mContext.getResources().getString(R.string.screenshot_saving_title)));
@@ -575,11 +585,11 @@
mScreenBitmap = screenshot;
- if (!isUserSetupComplete()) {
+ if (!isUserSetupComplete(owner)) {
Log.w(TAG, "User setup not complete, displaying toast only");
// User setup isn't complete, so we don't want to show any UI beyond a toast, as editing
// and sharing shouldn't be exposed to the user.
- saveScreenshotAndToast(finisher);
+ saveScreenshotAndToast(owner, finisher);
return;
}
@@ -587,7 +597,7 @@
mScreenBitmap.setHasAlpha(false);
mScreenBitmap.prepareToDraw();
- saveScreenshotInWorkerThread(finisher, this::showUiOnActionsReady,
+ saveScreenshotInWorkerThread(owner, finisher, this::showUiOnActionsReady,
this::showUiOnQuickShareActionReady);
// The window is focusable by default
@@ -853,11 +863,12 @@
* Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on
* failure).
*/
- private void saveScreenshotAndToast(Consumer<Uri> finisher) {
+ private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) {
// Play the shutter sound to notify that we've taken a screenshot
playCameraSound();
saveScreenshotInWorkerThread(
+ owner,
/* onComplete */ finisher,
/* actionsReadyListener */ imageData -> {
if (DEBUG_CALLBACK) {
@@ -925,9 +936,11 @@
/**
* Creates a new worker thread and saves the screenshot to the media store.
*/
- private void saveScreenshotInWorkerThread(Consumer<Uri> finisher,
- @Nullable ScreenshotController.ActionsReadyListener actionsReadyListener,
- @Nullable ScreenshotController.QuickShareActionReadyListener
+ private void saveScreenshotInWorkerThread(
+ UserHandle owner,
+ @NonNull Consumer<Uri> finisher,
+ @Nullable ActionsReadyListener actionsReadyListener,
+ @Nullable QuickShareActionReadyListener
quickShareActionsReadyListener) {
ScreenshotController.SaveImageInBackgroundData
data = new ScreenshotController.SaveImageInBackgroundData();
@@ -935,13 +948,14 @@
data.finisher = finisher;
data.mActionsReadyListener = actionsReadyListener;
data.mQuickShareActionsReadyListener = quickShareActionsReadyListener;
+ data.owner = owner;
if (mSaveInBgTask != null) {
// just log success/failure for the pre-existing screenshot
mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
}
- mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mImageExporter,
+ mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter,
mScreenshotSmartActions, data, getActionTransitionSupplier(),
mScreenshotNotificationSmartActionsProvider);
mSaveInBgTask.execute();
@@ -960,6 +974,15 @@
mScreenshotHandler.resetTimeout();
if (imageData.uri != null) {
+ if (!imageData.owner.equals(Process.myUserHandle())) {
+ // TODO: Handle non-primary user ownership (e.g. Work Profile)
+ // This image is owned by another user. Special treatment will be
+ // required in the UI (badging) as well as sending intents which can
+ // correctly forward those URIs on to be read (actions).
+
+ Log.d(TAG, "*** Screenshot saved to a non-primary user ("
+ + imageData.owner + ") as " + imageData.uri);
+ }
mScreenshotHandler.post(() -> {
if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
@@ -1033,9 +1056,9 @@
}
}
- private boolean isUserSetupComplete() {
- return Settings.Secure.getInt(mContext.getContentResolver(),
- SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
+ private boolean isUserSetupComplete(UserHandle owner) {
+ return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
+ .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
index c2a5060..3a35286 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
@@ -68,7 +68,9 @@
}
override suspend fun isManagedProfile(@UserIdInt userId: Int): Boolean {
- return withContext(bgDispatcher) { userMgr.isManagedProfile(userId) }
+ val managed = withContext(bgDispatcher) { userMgr.isManagedProfile(userId) }
+ Log.d(TAG, "isManagedProfile: $managed")
+ return managed
}
private fun nonPipVisibleTask(info: RootTaskInfo): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
index 83b60fb..30a0b8f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
@@ -78,6 +78,7 @@
static class LongScreenshot {
private final ImageTileSet mImageTileSet;
private final Session mSession;
+ // TODO: Add UserHandle so LongScreenshots can adhere to work profile screenshot policy
LongScreenshot(Session session, ImageTileSet imageTileSet) {
mSession = session;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index d7e86b6..7254e09 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -430,6 +430,7 @@
/**
* Determines if QS should be already expanded when expanding shade.
* Used for split shade, two finger gesture as well as accessibility shortcut to QS.
+ * It needs to be set when movement starts as it resets at the end of expansion/collapse.
*/
@VisibleForTesting
boolean mQsExpandImmediate;
@@ -1737,8 +1738,10 @@
}
private void setQsExpandImmediate(boolean expandImmediate) {
- mQsExpandImmediate = expandImmediate;
- mPanelEventsEmitter.notifyExpandImmediateChange(expandImmediate);
+ if (expandImmediate != mQsExpandImmediate) {
+ mQsExpandImmediate = expandImmediate;
+ mPanelEventsEmitter.notifyExpandImmediateChange(expandImmediate);
+ }
}
private void setShowShelfOnly(boolean shelfOnly) {
@@ -2479,17 +2482,23 @@
mDepthController.setQsPanelExpansion(qsExpansionFraction);
mStatusBarKeyguardViewManager.setQsExpansion(qsExpansionFraction);
- // updateQsExpansion will get called whenever mTransitionToFullShadeProgress or
- // mLockscreenShadeTransitionController.getDragProgress change.
- // When in lockscreen, getDragProgress indicates the true expanded fraction of QS
- float shadeExpandedFraction = mTransitioningToFullShadeProgress > 0
- ? mLockscreenShadeTransitionController.getQSDragProgress()
+ float shadeExpandedFraction = isOnKeyguard()
+ ? getLockscreenShadeDragProgress()
: getExpandedFraction();
mLargeScreenShadeHeaderController.setShadeExpandedFraction(shadeExpandedFraction);
mLargeScreenShadeHeaderController.setQsExpandedFraction(qsExpansionFraction);
mLargeScreenShadeHeaderController.setQsVisible(mQsVisible);
}
+ private float getLockscreenShadeDragProgress() {
+ // mTransitioningToFullShadeProgress > 0 means we're doing regular lockscreen to shade
+ // transition. If that's not the case we should follow QS expansion fraction for when
+ // user is pulling from the same top to go directly to expanded QS
+ return mTransitioningToFullShadeProgress > 0
+ ? mLockscreenShadeTransitionController.getQSDragProgress()
+ : computeQsExpansionFraction();
+ }
+
private void onStackYChanged(boolean shouldAnimate) {
if (mQs != null) {
if (shouldAnimate) {
@@ -3124,26 +3133,24 @@
}
if (mQsExpandImmediate || (mQsExpanded && !mQsTracking && mQsExpansionAnimator == null
&& !mQsExpansionFromOverscroll)) {
- float t;
- if (mKeyguardShowing) {
-
+ float qsExpansionFraction;
+ if (mSplitShadeEnabled) {
+ qsExpansionFraction = 1;
+ } else if (mKeyguardShowing) {
// On Keyguard, interpolate the QS expansion linearly to the panel expansion
- t = expandedHeight / (getMaxPanelHeight());
+ qsExpansionFraction = expandedHeight / (getMaxPanelHeight());
} else {
// In Shade, interpolate linearly such that QS is closed whenever panel height is
// minimum QS expansion + minStackHeight
- float
- panelHeightQsCollapsed =
+ float panelHeightQsCollapsed =
mNotificationStackScrollLayoutController.getIntrinsicPadding()
+ mNotificationStackScrollLayoutController.getLayoutMinHeight();
float panelHeightQsExpanded = calculatePanelHeightQsExpanded();
- t =
- (expandedHeight - panelHeightQsCollapsed) / (panelHeightQsExpanded
- - panelHeightQsCollapsed);
+ qsExpansionFraction = (expandedHeight - panelHeightQsCollapsed)
+ / (panelHeightQsExpanded - panelHeightQsCollapsed);
}
- float
- targetHeight =
- mQsMinExpansionHeight + t * (mQsMaxExpansionHeight - mQsMinExpansionHeight);
+ float targetHeight = mQsMinExpansionHeight
+ + qsExpansionFraction * (mQsMaxExpansionHeight - mQsMinExpansionHeight);
setQsExpansion(targetHeight);
}
updateExpandedHeight(expandedHeight);
@@ -3329,7 +3336,11 @@
} else {
setListening(true);
}
- setQsExpandImmediate(false);
+ if (mBarState != SHADE) {
+ // updating qsExpandImmediate is done in onPanelStateChanged for unlocked shade but
+ // on keyguard panel state is always OPEN so we need to have that extra update
+ setQsExpandImmediate(false);
+ }
setShowShelfOnly(false);
mTwoFingerQsExpandPossible = false;
updateTrackingHeadsUp(null);
@@ -4678,6 +4689,11 @@
}
}
} else {
+ // this else branch means we are doing one of:
+ // - from KEYGUARD and SHADE (but not expanded shade)
+ // - from SHADE to KEYGUARD
+ // - from SHADE_LOCKED to SHADE
+ // - getting notified again about the current SHADE or KEYGUARD state
final boolean animatingUnlockedShadeToKeyguard = oldState == SHADE
&& statusBarState == KEYGUARD
&& mScreenOffAnimationController.isKeyguardShowDelayed();
@@ -4749,9 +4765,7 @@
@Override
public float getLockscreenShadeDragProgress() {
- return mTransitioningToFullShadeProgress > 0
- ? mLockscreenShadeTransitionController.getQSDragProgress()
- : computeQsExpansionFraction();
+ return NotificationPanelViewController.this.getLockscreenShadeDragProgress();
}
};
@@ -4988,6 +5002,7 @@
updateQSExpansionEnabledAmbient();
if (state == STATE_OPEN && mCurrentPanelState != state) {
+ setQsExpandImmediate(false);
mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
}
if (state == STATE_OPENING) {
@@ -5000,6 +5015,7 @@
mCentralSurfaces.makeExpandedVisible(false);
}
if (state == STATE_CLOSED) {
+ setQsExpandImmediate(false);
// Close the status bar in the next frame so we can show the end of the
// animation.
mView.post(mMaybeHideExpandedRunnable);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 8278b54..ccf6fec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -393,7 +393,7 @@
val posted = mPostedEntries.compute(entry.key) { _, value ->
value?.also { update ->
update.wasUpdated = true
- update.shouldHeadsUpEver = update.shouldHeadsUpEver || shouldHeadsUpEver
+ update.shouldHeadsUpEver = shouldHeadsUpEver
update.shouldHeadsUpAgain = update.shouldHeadsUpAgain || shouldHeadsUpAgain
update.isAlerting = isAlerting
update.isBinding = isBinding
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
index 9faef1b..5ca13c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java
@@ -45,11 +45,21 @@
void logPanelShown(boolean isLockscreen,
@Nullable List<NotificationEntry> visibleNotifications);
+ /**
+ * Log a NOTIFICATION_PANEL_REPORTED statsd event, with
+ * {@link NotificationPanelEvent#NOTIFICATION_DRAG} as the eventID.
+ *
+ * @param draggedNotification the notification that is being dragged
+ */
+ void logNotificationDrag(NotificationEntry draggedNotification);
+
enum NotificationPanelEvent implements UiEventLogger.UiEventEnum {
@UiEvent(doc = "Notification panel shown from status bar.")
NOTIFICATION_PANEL_OPEN_STATUS_BAR(200),
@UiEvent(doc = "Notification panel shown from lockscreen.")
- NOTIFICATION_PANEL_OPEN_LOCKSCREEN(201);
+ NOTIFICATION_PANEL_OPEN_LOCKSCREEN(201),
+ @UiEvent(doc = "Notification was dragged")
+ NOTIFICATION_DRAG(1226);
private final int mId;
NotificationPanelEvent(int id) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
index 75a6019..9a63228 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java
@@ -16,12 +16,15 @@
package com.android.systemui.statusbar.notification.logging;
+import static com.android.systemui.statusbar.notification.logging.NotificationPanelLogger.NotificationPanelEvent.NOTIFICATION_DRAG;
+
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.logging.nano.Notifications;
import com.google.protobuf.nano.MessageNano;
+import java.util.Collections;
import java.util.List;
/**
@@ -38,4 +41,14 @@
/* int num_notifications*/ proto.notifications.length,
/* byte[] notifications*/ MessageNano.toByteArray(proto));
}
+
+ @Override
+ public void logNotificationDrag(NotificationEntry draggedNotification) {
+ final Notifications.NotificationList proto = NotificationPanelLogger.toNotificationProto(
+ Collections.singletonList(draggedNotification));
+ SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED,
+ /* int event_id */ NOTIFICATION_DRAG.getId(),
+ /* int num_notifications*/ proto.notifications.length,
+ /* byte[] notifications*/ MessageNano.toByteArray(proto));
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
index 4939a9c..64f87ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java
@@ -45,12 +45,17 @@
import androidx.annotation.VisibleForTesting;
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.InstanceIdSequence;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger;
import com.android.systemui.statusbar.policy.HeadsUpManager;
+import java.util.Collections;
+
import javax.inject.Inject;
/**
@@ -63,14 +68,17 @@
private final Context mContext;
private final HeadsUpManager mHeadsUpManager;
private final ShadeController mShadeController;
+ private NotificationPanelLogger mNotificationPanelLogger;
@Inject
public ExpandableNotificationRowDragController(Context context,
HeadsUpManager headsUpManager,
- ShadeController shadeController) {
+ ShadeController shadeController,
+ NotificationPanelLogger notificationPanelLogger) {
mContext = context;
mHeadsUpManager = headsUpManager;
mShadeController = shadeController;
+ mNotificationPanelLogger = notificationPanelLogger;
init();
}
@@ -120,12 +128,16 @@
dragIntent.putExtra(ClipDescription.EXTRA_PENDING_INTENT, contentIntent);
dragIntent.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle());
ClipData.Item item = new ClipData.Item(dragIntent);
+ InstanceId instanceId = new InstanceIdSequence(Integer.MAX_VALUE).newInstanceId();
+ item.getIntent().putExtra(ClipDescription.EXTRA_LOGGING_INSTANCE_ID, instanceId);
ClipData dragData = new ClipData(clipDescription, item);
View.DragShadowBuilder myShadow = new View.DragShadowBuilder(snapshot);
view.setOnDragListener(getDraggedViewDragListener());
boolean result = view.startDragAndDrop(dragData, myShadow, null, View.DRAG_FLAG_GLOBAL
| View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION);
if (result) {
+ // Log notification drag only if it succeeds
+ mNotificationPanelLogger.logNotificationDrag(enr.getEntry());
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
if (enr.isPinned()) {
mHeadsUpManager.releaseAllImmediately();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java
index bc172ce..0b435fe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSection.java
@@ -35,12 +35,12 @@
* bounds change.
*/
public class NotificationSection {
- private @PriorityBucket int mBucket;
- private View mOwningView;
- private Rect mBounds = new Rect();
- private Rect mCurrentBounds = new Rect(-1, -1, -1, -1);
- private Rect mStartAnimationRect = new Rect();
- private Rect mEndAnimationRect = new Rect();
+ private @PriorityBucket final int mBucket;
+ private final View mOwningView;
+ private final Rect mBounds = new Rect();
+ private final Rect mCurrentBounds = new Rect(-1, -1, -1, -1);
+ private final Rect mStartAnimationRect = new Rect();
+ private final Rect mEndAnimationRect = new Rect();
private ObjectAnimator mTopAnimator = null;
private ObjectAnimator mBottomAnimator = null;
private ExpandableView mFirstVisibleChild;
@@ -277,7 +277,6 @@
}
}
}
- top = Math.max(minTopPosition, top);
ExpandableView lastView = getLastVisibleChild();
if (lastView != null) {
float finalTranslationY = ViewState.getFinalTranslationY(lastView);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 5fbaa51..e377501 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -135,7 +135,7 @@
private static final boolean SPEW = Log.isLoggable(TAG, Log.VERBOSE);
// Delay in milli-seconds before shade closes for clear all.
- private final int DELAY_BEFORE_SHADE_CLOSE = 200;
+ private static final int DELAY_BEFORE_SHADE_CLOSE = 200;
private boolean mShadeNeedsToClose = false;
private static final float RUBBER_BAND_FACTOR_NORMAL = 0.35f;
@@ -152,7 +152,7 @@
private static final int DISTANCE_BETWEEN_ADJACENT_SECTIONS_PX = 1;
private boolean mKeyguardBypassEnabled;
- private ExpandHelper mExpandHelper;
+ private final ExpandHelper mExpandHelper;
private NotificationSwipeHelper mSwipeHelper;
private int mCurrentStackHeight = Integer.MAX_VALUE;
private final Paint mBackgroundPaint = new Paint();
@@ -165,12 +165,7 @@
private VelocityTracker mVelocityTracker;
private OverScroller mScroller;
- /** Last Y position reported by {@link #mScroller}, used to calculate scroll delta. */
- private int mLastScrollerY;
- /**
- * True if the max position was set to a known position on the last call to {@link #mScroller}.
- */
- private boolean mIsScrollerBoundSet;
+
private Runnable mFinishScrollingCallback;
private int mTouchSlop;
private float mSlopMultiplier;
@@ -194,7 +189,6 @@
private int mContentHeight;
private float mIntrinsicContentHeight;
- private int mCollapsedSize;
private int mPaddingBetweenElements;
private int mMaxTopPadding;
private int mTopPadding;
@@ -210,15 +204,15 @@
private final StackScrollAlgorithm mStackScrollAlgorithm;
private final AmbientState mAmbientState;
- private GroupMembershipManager mGroupMembershipManager;
- private GroupExpansionManager mGroupExpansionManager;
- private HashSet<ExpandableView> mChildrenToAddAnimated = new HashSet<>();
- private ArrayList<View> mAddedHeadsUpChildren = new ArrayList<>();
- private ArrayList<ExpandableView> mChildrenToRemoveAnimated = new ArrayList<>();
- private ArrayList<ExpandableView> mChildrenChangingPositions = new ArrayList<>();
- private HashSet<View> mFromMoreCardAdditions = new HashSet<>();
- private ArrayList<AnimationEvent> mAnimationEvents = new ArrayList<>();
- private ArrayList<View> mSwipedOutViews = new ArrayList<>();
+ private final GroupMembershipManager mGroupMembershipManager;
+ private final GroupExpansionManager mGroupExpansionManager;
+ private final HashSet<ExpandableView> mChildrenToAddAnimated = new HashSet<>();
+ private final ArrayList<View> mAddedHeadsUpChildren = new ArrayList<>();
+ private final ArrayList<ExpandableView> mChildrenToRemoveAnimated = new ArrayList<>();
+ private final ArrayList<ExpandableView> mChildrenChangingPositions = new ArrayList<>();
+ private final HashSet<View> mFromMoreCardAdditions = new HashSet<>();
+ private final ArrayList<AnimationEvent> mAnimationEvents = new ArrayList<>();
+ private final ArrayList<View> mSwipedOutViews = new ArrayList<>();
private NotificationStackSizeCalculator mNotificationStackSizeCalculator;
private final StackStateAnimator mStateAnimator = new StackStateAnimator(this);
private boolean mAnimationsEnabled;
@@ -296,7 +290,7 @@
private boolean mDisallowDismissInThisMotion;
private boolean mDisallowScrollingInThisMotion;
private long mGoToFullShadeDelay;
- private ViewTreeObserver.OnPreDrawListener mChildrenUpdater
+ private final ViewTreeObserver.OnPreDrawListener mChildrenUpdater
= new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
@@ -309,17 +303,16 @@
};
private NotificationStackScrollLogger mLogger;
private CentralSurfaces mCentralSurfaces;
- private int[] mTempInt2 = new int[2];
+ private final int[] mTempInt2 = new int[2];
private boolean mGenerateChildOrderChangedEvent;
- private HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>();
- private HashSet<ExpandableView> mClearTransientViewsWhenFinished = new HashSet<>();
- private HashSet<Pair<ExpandableNotificationRow, Boolean>> mHeadsUpChangeAnimations
+ private final HashSet<Runnable> mAnimationFinishedRunnables = new HashSet<>();
+ private final HashSet<ExpandableView> mClearTransientViewsWhenFinished = new HashSet<>();
+ private final HashSet<Pair<ExpandableNotificationRow, Boolean>> mHeadsUpChangeAnimations
= new HashSet<>();
- private boolean mTrackingHeadsUp;
private boolean mForceNoOverlappingRendering;
private final ArrayList<Pair<ExpandableNotificationRow, Boolean>> mTmpList = new ArrayList<>();
private boolean mAnimationRunning;
- private ViewTreeObserver.OnPreDrawListener mRunningAnimationUpdater
+ private final ViewTreeObserver.OnPreDrawListener mRunningAnimationUpdater
= new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
@@ -327,21 +320,21 @@
return true;
}
};
- private NotificationSection[] mSections;
+ private final NotificationSection[] mSections;
private boolean mAnimateNextBackgroundTop;
private boolean mAnimateNextBackgroundBottom;
private boolean mAnimateNextSectionBoundsChange;
private int mBgColor;
private float mDimAmount;
private ValueAnimator mDimAnimator;
- private ArrayList<ExpandableView> mTmpSortedChildren = new ArrayList<>();
+ private final ArrayList<ExpandableView> mTmpSortedChildren = new ArrayList<>();
private final Animator.AnimatorListener mDimEndListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mDimAnimator = null;
}
};
- private ValueAnimator.AnimatorUpdateListener mDimUpdateListener
+ private final ValueAnimator.AnimatorUpdateListener mDimUpdateListener
= new ValueAnimator.AnimatorUpdateListener() {
@Override
@@ -351,29 +344,23 @@
};
protected ViewGroup mQsHeader;
// Rect of QsHeader. Kept as a field just to avoid creating a new one each time.
- private Rect mQsHeaderBound = new Rect();
+ private final Rect mQsHeaderBound = new Rect();
private boolean mContinuousShadowUpdate;
private boolean mContinuousBackgroundUpdate;
- private ViewTreeObserver.OnPreDrawListener mShadowUpdater
+ private final ViewTreeObserver.OnPreDrawListener mShadowUpdater
= () -> {
updateViewShadows();
return true;
};
- private ViewTreeObserver.OnPreDrawListener mBackgroundUpdater = () -> {
+ private final ViewTreeObserver.OnPreDrawListener mBackgroundUpdater = () -> {
updateBackground();
return true;
};
- private Comparator<ExpandableView> mViewPositionComparator = (view, otherView) -> {
+ private final Comparator<ExpandableView> mViewPositionComparator = (view, otherView) -> {
float endY = view.getTranslationY() + view.getActualHeight();
float otherEndY = otherView.getTranslationY() + otherView.getActualHeight();
- if (endY < otherEndY) {
- return -1;
- } else if (endY > otherEndY) {
- return 1;
- } else {
- // The two notifications end at the same location
- return 0;
- }
+ // Return zero when the two notifications end at the same location
+ return Float.compare(endY, otherEndY);
};
private final ViewOutlineProvider mOutlineProvider = new ViewOutlineProvider() {
@Override
@@ -435,16 +422,14 @@
private int mUpcomingStatusBarState;
private int mCachedBackgroundColor;
private boolean mHeadsUpGoingAwayAnimationsAllowed = true;
- private Runnable mReflingAndAnimateScroll = () -> {
- animateScroll();
- };
+ private final Runnable mReflingAndAnimateScroll = this::animateScroll;
private int mCornerRadius;
private int mMinimumPaddings;
private int mQsTilePadding;
private boolean mSkinnyNotifsInLandscape;
private int mSidePaddings;
private final Rect mBackgroundAnimationRect = new Rect();
- private ArrayList<BiConsumer<Float, Float>> mExpandedHeightListeners = new ArrayList<>();
+ private final ArrayList<BiConsumer<Float, Float>> mExpandedHeightListeners = new ArrayList<>();
private int mHeadsUpInset;
/**
@@ -479,8 +464,6 @@
private int mWaterfallTopInset;
private NotificationStackScrollLayoutController mController;
- private boolean mKeyguardMediaControllorVisible;
-
/**
* The clip path used to clip the view in a rounded way.
*/
@@ -501,7 +484,7 @@
private int mRoundedRectClippingTop;
private int mRoundedRectClippingBottom;
private int mRoundedRectClippingRight;
- private float[] mBgCornerRadii = new float[8];
+ private final float[] mBgCornerRadii = new float[8];
/**
* Whether stackY should be animated in case the view is getting shorter than the scroll
@@ -527,7 +510,7 @@
/**
* Corner radii of the launched notification if it's clipped
*/
- private float[] mLaunchedNotificationRadii = new float[8];
+ private final float[] mLaunchedNotificationRadii = new float[8];
/**
* The notification that is being launched currently.
@@ -779,7 +762,7 @@
y = getLayoutHeight();
drawDebugInfo(canvas, y, Color.YELLOW, /* label= */ "getLayoutHeight() = " + y);
- y = (int) mMaxLayoutHeight;
+ y = mMaxLayoutHeight;
drawDebugInfo(canvas, y, Color.MAGENTA, /* label= */ "mMaxLayoutHeight = " + y);
// The space between mTopPadding and mKeyguardBottomPadding determines the available space
@@ -997,7 +980,6 @@
mOverflingDistance = configuration.getScaledOverflingDistance();
Resources res = context.getResources();
- mCollapsedSize = res.getDimensionPixelSize(R.dimen.notification_min_height);
mGapHeight = res.getDimensionPixelSize(R.dimen.notification_section_divider_height);
mStackScrollAlgorithm.initView(context);
mAmbientState.reload(context);
@@ -1256,12 +1238,9 @@
private void clampScrollPosition() {
int scrollRange = getScrollRange();
if (scrollRange < mOwnScrollY && !mAmbientState.isClearAllInProgress()) {
- boolean animateStackY = false;
- if (scrollRange < getScrollAmountToScrollBoundary()
- && mAnimateStackYForContentHeightChange) {
- // if the scroll boundary updates the position of the stack,
- animateStackY = true;
- }
+ // if the scroll boundary updates the position of the stack,
+ boolean animateStackY = scrollRange < getScrollAmountToScrollBoundary()
+ && mAnimateStackYForContentHeightChange;
setOwnScrollY(scrollRange, animateStackY);
}
}
@@ -1504,7 +1483,6 @@
}
if (mAmbientState.isHiddenAtAll()) {
- clipToOutline = false;
invalidateOutline();
if (isFullyHidden()) {
setClipBounds(null);
@@ -1782,7 +1760,7 @@
}
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
- private Runnable mReclamp = new Runnable() {
+ private final Runnable mReclamp = new Runnable() {
@Override
public void run() {
int range = getScrollRange();
@@ -3084,11 +3062,8 @@
int currentIndex = indexOfChild(child);
if (currentIndex == -1) {
- boolean isTransient = false;
- if (child instanceof ExpandableNotificationRow
- && child.getTransientContainer() != null) {
- isTransient = true;
- }
+ boolean isTransient = child instanceof ExpandableNotificationRow
+ && child.getTransientContainer() != null;
Log.e(TAG, "Attempting to re-position "
+ (isTransient ? "transient" : "")
+ " view {"
@@ -3149,7 +3124,6 @@
private void generateHeadsUpAnimationEvents() {
for (Pair<ExpandableNotificationRow, Boolean> eventPair : mHeadsUpChangeAnimations) {
ExpandableNotificationRow row = eventPair.first;
- String key = row.getEntry().getKey();
boolean isHeadsUp = eventPair.second;
if (isHeadsUp != row.isHeadsUp()) {
// For cases where we have a heads up showing and appearing again we shouldn't
@@ -3212,10 +3186,8 @@
@ShadeViewRefactor(RefactorComponent.COORDINATOR)
private boolean shouldHunAppearFromBottom(ExpandableViewState viewState) {
- if (viewState.yTranslation + viewState.height < mAmbientState.getMaxHeadsUpTranslation()) {
- return false;
- }
- return true;
+ return viewState.yTranslation + viewState.height
+ >= mAmbientState.getMaxHeadsUpTranslation();
}
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
@@ -4790,7 +4762,6 @@
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
public void setTrackingHeadsUp(ExpandableNotificationRow row) {
mAmbientState.setTrackedHeadsUpRow(row);
- mTrackingHeadsUp = row != null;
}
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
@@ -6176,7 +6147,7 @@
}
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
- private ExpandHelper.Callback mExpandHelperCallback = new ExpandHelper.Callback() {
+ private final ExpandHelper.Callback mExpandHelperCallback = new ExpandHelper.Callback() {
@Override
public ExpandableView getChildAtPosition(float touchX, float touchY) {
return NotificationStackScrollLayout.this.getChildAtPosition(touchX, touchY);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
index 0995a00..712953e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
@@ -505,7 +505,7 @@
v.bind(name, drawable, item.info.id);
}
v.setActivated(item.isCurrent);
- v.setDisabledByAdmin(getController().isDisabledByAdmin(item));
+ v.setDisabledByAdmin(item.isDisabledByAdmin());
v.setEnabled(item.isSwitchToEnabled);
UserSwitcherController.setSelectableAlpha(v);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
index 843c232..146b222 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
@@ -19,7 +19,6 @@
import android.annotation.UserIdInt
import android.content.Intent
import android.view.View
-import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
import com.android.systemui.Dumpable
import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
import com.android.systemui.user.data.source.UserRecord
@@ -130,12 +129,6 @@
/** Whether keyguard is showing. */
val isKeyguardShowing: Boolean
- /** Returns the [EnforcedAdmin] for the given record, or `null` if there isn't one. */
- fun getEnforcedAdmin(record: UserRecord): EnforcedAdmin?
-
- /** Returns `true` if the given record is disabled by the admin; `false` otherwise. */
- fun isDisabledByAdmin(record: UserRecord): Boolean
-
/** Starts an activity with the given [Intent]. */
fun startActivity(intent: Intent)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
index 12834f6..1692656 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
@@ -17,13 +17,21 @@
package com.android.systemui.statusbar.policy
+import android.content.Context
import android.content.Intent
import android.view.View
-import com.android.settingslib.RestrictedLockUtils
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.user.UserSwitchDialogController
import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.domain.interactor.GuestUserInteractor
+import com.android.systemui.user.domain.interactor.UserInteractor
+import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper
+import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import dagger.Lazy
import java.io.PrintWriter
import java.lang.ref.WeakReference
@@ -31,58 +39,76 @@
import kotlinx.coroutines.flow.Flow
/** Implementation of [UserSwitcherController]. */
+@SysUISingleton
class UserSwitcherControllerImpl
@Inject
constructor(
- private val flags: FeatureFlags,
+ @Application private val applicationContext: Context,
+ flags: FeatureFlags,
@Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>,
+ private val userInteractorLazy: Lazy<UserInteractor>,
+ private val guestUserInteractorLazy: Lazy<GuestUserInteractor>,
+ private val keyguardInteractorLazy: Lazy<KeyguardInteractor>,
+ private val activityStarter: ActivityStarter,
) : UserSwitcherController {
- private val isNewImpl: Boolean
- get() = flags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER)
+ private val useInteractor: Boolean =
+ flags.isEnabled(Flags.USER_CONTROLLER_USES_INTERACTOR) &&
+ !flags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
private val _oldImpl: UserSwitcherControllerOldImpl
get() = oldImpl.get()
+ private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() }
+ private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() }
+ private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() }
- private fun notYetImplemented(): Nothing {
- error("Not yet implemented!")
+ private val callbackCompatMap =
+ mutableMapOf<UserSwitcherController.UserSwitchCallback, UserInteractor.UserCallback>()
+
+ private fun notSupported(): Nothing {
+ error("Not supported in the new implementation!")
}
override val users: ArrayList<UserRecord>
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.userRecords.value
} else {
_oldImpl.users
}
override val isSimpleUserSwitcher: Boolean
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.isSimpleUserSwitcher
} else {
_oldImpl.isSimpleUserSwitcher
}
override fun init(view: View) {
- if (isNewImpl) {
- notYetImplemented()
- } else {
+ if (!useInteractor) {
_oldImpl.init(view)
}
}
override val currentUserRecord: UserRecord?
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.selectedUserRecord.value
} else {
_oldImpl.currentUserRecord
}
override val currentUserName: String?
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ currentUserRecord?.let {
+ LegacyUserUiHelper.getUserRecordName(
+ context = applicationContext,
+ record = it,
+ isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
+ isGuestUserResetting = userInteractor.isGuestUserResetting,
+ )
+ }
} else {
_oldImpl.currentUserName
}
@@ -91,8 +117,8 @@
userId: Int,
dialogShower: UserSwitchDialogController.DialogShower?
) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.selectUser(userId)
} else {
_oldImpl.onUserSelected(userId, dialogShower)
}
@@ -100,24 +126,24 @@
override val isAddUsersFromLockScreenEnabled: Flow<Boolean>
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ notSupported()
} else {
_oldImpl.isAddUsersFromLockScreenEnabled
}
override val isGuestUserAutoCreated: Boolean
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.isGuestUserAutoCreated
} else {
_oldImpl.isGuestUserAutoCreated
}
override val isGuestUserResetting: Boolean
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.isGuestUserResetting
} else {
_oldImpl.isGuestUserResetting
}
@@ -125,40 +151,48 @@
override fun createAndSwitchToGuestUser(
dialogShower: UserSwitchDialogController.DialogShower?,
) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ notSupported()
} else {
_oldImpl.createAndSwitchToGuestUser(dialogShower)
}
}
override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ notSupported()
} else {
_oldImpl.showAddUserDialog(dialogShower)
}
}
override fun startSupervisedUserActivity() {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ notSupported()
} else {
_oldImpl.startSupervisedUserActivity()
}
}
override fun onDensityOrFontScaleChanged() {
- if (isNewImpl) {
- notYetImplemented()
- } else {
+ if (!useInteractor) {
_oldImpl.onDensityOrFontScaleChanged()
}
}
override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.addCallback(
+ object : UserInteractor.UserCallback {
+ override fun isEvictable(): Boolean {
+ return adapter.get() == null
+ }
+
+ override fun onUserStateChanged() {
+ adapter.get()?.notifyDataSetChanged()
+ }
+ }
+ )
} else {
_oldImpl.addAdapter(adapter)
}
@@ -168,16 +202,23 @@
record: UserRecord,
dialogShower: UserSwitchDialogController.DialogShower?,
) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ if (LegacyUserDataHelper.isUser(record)) {
+ userInteractor.selectUser(record.resolveId())
+ } else {
+ userInteractor.executeAction(LegacyUserDataHelper.toUserActionModel(record))
+ }
} else {
_oldImpl.onUserListItemClicked(record, dialogShower)
}
}
override fun removeGuestUser(guestUserId: Int, targetUserId: Int) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.removeGuestUser(
+ guestUserId = guestUserId,
+ targetUserId = targetUserId,
+ )
} else {
_oldImpl.removeGuestUser(guestUserId, targetUserId)
}
@@ -188,16 +229,16 @@
targetUserId: Int,
forceRemoveGuestOnExit: Boolean
) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
} else {
_oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
}
}
override fun schedulePostBootGuestCreation() {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ guestUserInteractor.onDeviceBootCompleted()
} else {
_oldImpl.schedulePostBootGuestCreation()
}
@@ -205,63 +246,57 @@
override val isKeyguardShowing: Boolean
get() =
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ keyguardInteractor.isKeyguardShowing()
} else {
_oldImpl.isKeyguardShowing
}
- override fun getEnforcedAdmin(record: UserRecord): RestrictedLockUtils.EnforcedAdmin? {
- return if (isNewImpl) {
- notYetImplemented()
- } else {
- _oldImpl.getEnforcedAdmin(record)
- }
- }
-
- override fun isDisabledByAdmin(record: UserRecord): Boolean {
- return if (isNewImpl) {
- notYetImplemented()
- } else {
- _oldImpl.isDisabledByAdmin(record)
- }
- }
-
override fun startActivity(intent: Intent) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ activityStarter.startActivity(intent, /* dismissShade= */ false)
} else {
_oldImpl.startActivity(intent)
}
}
override fun refreshUsers(forcePictureLoadForId: Int) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.refreshUsers()
} else {
_oldImpl.refreshUsers(forcePictureLoadForId)
}
}
override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ val interactorCallback =
+ object : UserInteractor.UserCallback {
+ override fun onUserStateChanged() {
+ callback.onUserSwitched()
+ }
+ }
+ callbackCompatMap[callback] = interactorCallback
+ userInteractor.addCallback(interactorCallback)
} else {
_oldImpl.addUserSwitchCallback(callback)
}
}
override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ val interactorCallback = callbackCompatMap.remove(callback)
+ if (interactorCallback != null) {
+ userInteractor.removeCallback(interactorCallback)
+ }
} else {
_oldImpl.removeUserSwitchCallback(callback)
}
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
- if (isNewImpl) {
- notYetImplemented()
+ if (useInteractor) {
+ userInteractor.dump(pw)
} else {
_oldImpl.dump(pw, args)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
index d365aa6..46d2f3a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
@@ -17,17 +17,13 @@
import static android.os.UserManager.SWITCHABILITY_STATUS_OK;
-import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
-
import android.annotation.UserIdInt;
-import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.IActivityManager;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
import android.content.Context;
-import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.UserInfo;
@@ -40,7 +36,6 @@
import android.provider.Settings;
import android.telephony.TelephonyCallback;
import android.text.TextUtils;
-import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
@@ -49,17 +44,14 @@
import android.widget.Toast;
import androidx.annotation.Nullable;
-import androidx.collection.SimpleArrayMap;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.util.LatencyTracker;
-import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.users.UserCreatingDialog;
import com.android.systemui.GuestResetOrExitSessionReceiver;
import com.android.systemui.GuestResumeSessionReceiver;
-import com.android.systemui.R;
import com.android.systemui.SystemUISecondaryUserService;
import com.android.systemui.animation.DialogCuj;
import com.android.systemui.animation.DialogLaunchAnimator;
@@ -75,10 +67,12 @@
import com.android.systemui.qs.QSUserSwitcherEvent;
import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower;
import com.android.systemui.settings.UserTracker;
-import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.telephony.TelephonyListenerManager;
-import com.android.systemui.user.CreateUserActivity;
import com.android.systemui.user.data.source.UserRecord;
+import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper;
+import com.android.systemui.user.shared.model.UserActionModel;
+import com.android.systemui.user.ui.dialog.AddUserDialog;
+import com.android.systemui.user.ui.dialog.ExitGuestDialog;
import com.android.systemui.util.settings.GlobalSettings;
import com.android.systemui.util.settings.SecureSettings;
@@ -139,9 +133,6 @@
private final InteractionJankMonitor mInteractionJankMonitor;
private final LatencyTracker mLatencyTracker;
private final DialogLaunchAnimator mDialogLaunchAnimator;
- private final SimpleArrayMap<UserRecord, EnforcedAdmin> mEnforcedAdminByUserRecord =
- new SimpleArrayMap<>();
- private final ArraySet<UserRecord> mDisabledByAdmin = new ArraySet<>();
private ArrayList<UserRecord> mUsers = new ArrayList<>();
@VisibleForTesting
@@ -334,7 +325,6 @@
for (UserInfo info : infos) {
boolean isCurrent = currentId == info.id;
- boolean switchToEnabled = canSwitchUsers || isCurrent;
if (!mUserSwitcherEnabled && !info.isPrimary()) {
continue;
}
@@ -343,25 +333,22 @@
if (info.isGuest()) {
// Tapping guest icon triggers remove and a user switch therefore
// the icon shouldn't be enabled even if the user is current
- guestRecord = new UserRecord(info, null /* picture */,
- true /* isGuest */, isCurrent, false /* isAddUser */,
- false /* isRestricted */, canSwitchUsers,
- false /* isAddSupervisedUser */);
+ guestRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ mUserManager,
+ null /* picture */,
+ info,
+ isCurrent,
+ canSwitchUsers);
} else if (info.supportsSwitchToByUser()) {
- Bitmap picture = bitmaps.get(info.id);
- if (picture == null) {
- picture = mUserManager.getUserIcon(info.id);
-
- if (picture != null) {
- int avatarSize = mContext.getResources()
- .getDimensionPixelSize(R.dimen.max_avatar_size);
- picture = Bitmap.createScaledBitmap(
- picture, avatarSize, avatarSize, true);
- }
- }
- records.add(new UserRecord(info, picture, false /* isGuest */,
- isCurrent, false /* isAddUser */, false /* isRestricted */,
- switchToEnabled, false /* isAddSupervisedUser */));
+ records.add(
+ LegacyUserDataHelper.createRecord(
+ mContext,
+ mUserManager,
+ bitmaps.get(info.id),
+ info,
+ isCurrent,
+ canSwitchUsers));
}
}
}
@@ -372,18 +359,20 @@
// we will just use it as an indicator for "Resetting guest...".
// Otherwise, default to canSwitchUsers.
boolean isSwitchToGuestEnabled = !mGuestIsResetting.get() && canSwitchUsers;
- guestRecord = new UserRecord(null /* info */, null /* picture */,
- true /* isGuest */, false /* isCurrent */,
- false /* isAddUser */, false /* isRestricted */,
- isSwitchToGuestEnabled, false /* isAddSupervisedUser */);
- checkIfAddUserDisallowedByAdminOnly(guestRecord);
+ guestRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ currentId,
+ UserActionModel.ENTER_GUEST_MODE,
+ false /* isRestricted */,
+ isSwitchToGuestEnabled);
records.add(guestRecord);
} else if (canCreateGuest(guestRecord != null)) {
- guestRecord = new UserRecord(null /* info */, null /* picture */,
- true /* isGuest */, false /* isCurrent */,
- false /* isAddUser */, createIsRestricted(), canSwitchUsers,
- false /* isAddSupervisedUser */);
- checkIfAddUserDisallowedByAdminOnly(guestRecord);
+ guestRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ currentId,
+ UserActionModel.ENTER_GUEST_MODE,
+ false /* isRestricted */,
+ canSwitchUsers);
records.add(guestRecord);
}
} else {
@@ -391,20 +380,23 @@
}
if (canCreateUser()) {
- UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */,
- false /* isGuest */, false /* isCurrent */, true /* isAddUser */,
- createIsRestricted(), canSwitchUsers,
- false /* isAddSupervisedUser */);
- checkIfAddUserDisallowedByAdminOnly(addUserRecord);
- records.add(addUserRecord);
+ final UserRecord userRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ currentId,
+ UserActionModel.ADD_USER,
+ createIsRestricted(),
+ canSwitchUsers);
+ records.add(userRecord);
}
if (canCreateSupervisedUser()) {
- UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */,
- false /* isGuest */, false /* isCurrent */, false /* isAddUser */,
- createIsRestricted(), canSwitchUsers, true /* isAddSupervisedUser */);
- checkIfAddUserDisallowedByAdminOnly(addUserRecord);
- records.add(addUserRecord);
+ final UserRecord userRecord = LegacyUserDataHelper.createRecord(
+ mContext,
+ currentId,
+ UserActionModel.ADD_SUPERVISED_USER,
+ createIsRestricted(),
+ canSwitchUsers);
+ records.add(userRecord);
}
mUiExecutor.execute(() -> {
@@ -591,12 +583,23 @@
showExitGuestDialog(id, isGuestEphemeral, newId, dialogShower);
}
- private void showExitGuestDialog(int id, boolean isGuestEphemeral,
- int targetId, DialogShower dialogShower) {
+ private void showExitGuestDialog(
+ int id,
+ boolean isGuestEphemeral,
+ int targetId,
+ DialogShower dialogShower) {
if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) {
mExitGuestDialog.cancel();
}
- mExitGuestDialog = new ExitGuestDialog(mContext, id, isGuestEphemeral, targetId);
+ mExitGuestDialog = new ExitGuestDialog(
+ mContext,
+ id,
+ isGuestEphemeral,
+ targetId,
+ mKeyguardStateController.isShowing(),
+ mFalsingManager,
+ mDialogLaunchAnimator,
+ this::exitGuestUser);
if (dialogShower != null) {
dialogShower.showDialog(mExitGuestDialog, new DialogCuj(
InteractionJankMonitor.CUJ_USER_DIALOG_OPEN,
@@ -622,7 +625,15 @@
if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
mAddUserDialog.cancel();
}
- mAddUserDialog = new AddUserDialog(mContext);
+ final UserInfo currentUser = mUserTracker.getUserInfo();
+ mAddUserDialog = new AddUserDialog(
+ mContext,
+ currentUser.getUserHandle(),
+ mKeyguardStateController.isShowing(),
+ /* showEphemeralMessage= */currentUser.isGuest() && currentUser.isEphemeral(),
+ mFalsingManager,
+ mBroadcastSender,
+ mDialogLaunchAnimator);
if (dialogShower != null) {
dialogShower.showDialog(mAddUserDialog,
new DialogCuj(
@@ -964,30 +975,6 @@
return mKeyguardStateController.isShowing();
}
- @Override
- @Nullable
- public EnforcedAdmin getEnforcedAdmin(UserRecord record) {
- return mEnforcedAdminByUserRecord.get(record);
- }
-
- @Override
- public boolean isDisabledByAdmin(UserRecord record) {
- return mDisabledByAdmin.contains(record);
- }
-
- private void checkIfAddUserDisallowedByAdminOnly(UserRecord record) {
- EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
- UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId());
- if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext,
- UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId())) {
- mDisabledByAdmin.add(record);
- mEnforcedAdminByUserRecord.put(record, admin);
- } else {
- mDisabledByAdmin.remove(record);
- mEnforcedAdminByUserRecord.put(record, null);
- }
- }
-
private boolean shouldUseSimpleUserSwitcher() {
int defaultSimpleUserSwitcher = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_expandLockScreenUserSwitcher) ? 1 : 0;
@@ -1052,133 +1039,4 @@
}
}
};
-
-
- private final class ExitGuestDialog extends SystemUIDialog implements
- DialogInterface.OnClickListener {
-
- private final int mGuestId;
- private final int mTargetId;
- private final boolean mIsGuestEphemeral;
-
- ExitGuestDialog(Context context, int guestId, boolean isGuestEphemeral,
- int targetId) {
- super(context);
- if (isGuestEphemeral) {
- setTitle(context.getString(
- com.android.settingslib.R.string.guest_exit_dialog_title));
- setMessage(context.getString(
- com.android.settingslib.R.string.guest_exit_dialog_message));
- setButton(DialogInterface.BUTTON_NEUTRAL,
- context.getString(android.R.string.cancel), this);
- setButton(DialogInterface.BUTTON_POSITIVE,
- context.getString(
- com.android.settingslib.R.string.guest_exit_dialog_button), this);
- } else {
- setTitle(context.getString(
- com.android.settingslib
- .R.string.guest_exit_dialog_title_non_ephemeral));
- setMessage(context.getString(
- com.android.settingslib
- .R.string.guest_exit_dialog_message_non_ephemeral));
- setButton(DialogInterface.BUTTON_NEUTRAL,
- context.getString(android.R.string.cancel), this);
- setButton(DialogInterface.BUTTON_NEGATIVE,
- context.getString(
- com.android.settingslib.R.string.guest_exit_clear_data_button),
- this);
- setButton(DialogInterface.BUTTON_POSITIVE,
- context.getString(
- com.android.settingslib.R.string.guest_exit_save_data_button),
- this);
- }
- SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing());
- setCanceledOnTouchOutside(false);
- mGuestId = guestId;
- mTargetId = targetId;
- mIsGuestEphemeral = isGuestEphemeral;
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY
- : FalsingManager.HIGH_PENALTY;
- if (mFalsingManager.isFalseTap(penalty)) {
- return;
- }
- if (mIsGuestEphemeral) {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- mDialogLaunchAnimator.dismissStack(this);
- // Ephemeral guest: exit guest, guest is removed by the system
- // on exit, since its marked ephemeral
- exitGuestUser(mGuestId, mTargetId, false);
- } else if (which == DialogInterface.BUTTON_NEGATIVE) {
- // Cancel clicked, do nothing
- cancel();
- }
- } else {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- mDialogLaunchAnimator.dismissStack(this);
- // Non-ephemeral guest: exit guest, guest is not removed by the system
- // on exit, since its marked non-ephemeral
- exitGuestUser(mGuestId, mTargetId, false);
- } else if (which == DialogInterface.BUTTON_NEGATIVE) {
- mDialogLaunchAnimator.dismissStack(this);
- // Non-ephemeral guest: remove guest and then exit
- exitGuestUser(mGuestId, mTargetId, true);
- } else if (which == DialogInterface.BUTTON_NEUTRAL) {
- // Cancel clicked, do nothing
- cancel();
- }
- }
- }
- }
-
- @VisibleForTesting
- final class AddUserDialog extends SystemUIDialog implements
- DialogInterface.OnClickListener {
-
- AddUserDialog(Context context) {
- super(context);
-
- setTitle(com.android.settingslib.R.string.user_add_user_title);
- String message = context.getString(
- com.android.settingslib.R.string.user_add_user_message_short);
- UserInfo currentUser = mUserTracker.getUserInfo();
- if (currentUser != null && currentUser.isGuest() && currentUser.isEphemeral()) {
- message += context.getString(R.string.user_add_user_message_guest_remove);
- }
- setMessage(message);
- setButton(DialogInterface.BUTTON_NEUTRAL,
- context.getString(android.R.string.cancel), this);
- setButton(DialogInterface.BUTTON_POSITIVE,
- context.getString(android.R.string.ok), this);
- SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing());
- }
-
- @Override
- public void onClick(DialogInterface dialog, int which) {
- int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY
- : FalsingManager.MODERATE_PENALTY;
- if (mFalsingManager.isFalseTap(penalty)) {
- return;
- }
- if (which == BUTTON_NEUTRAL) {
- cancel();
- } else {
- mDialogLaunchAnimator.dismissStack(this);
- if (ActivityManager.isUserAMonkey()) {
- return;
- }
- // Use broadcast instead of ShadeController, as this dialog may have started in
- // another process and normal dagger bindings are not available
- mBroadcastSender.sendBroadcastAsUser(
- new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), UserHandle.CURRENT);
- getContext().startActivityAsUser(
- CreateUserActivity.createIntentForStart(getContext()),
- mUserTracker.getUserHandle());
- }
- }
- }
-
}
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt
new file mode 100644
index 0000000..9c38dc0f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2022 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.telephony.data.repository
+
+import android.telephony.Annotation
+import android.telephony.TelephonyCallback
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.telephony.TelephonyListenerManager
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Defines interface for classes that encapsulate _some_ telephony-related state. */
+interface TelephonyRepository {
+ /** The state of the current call. */
+ @Annotation.CallState val callState: Flow<Int>
+}
+
+/**
+ * NOTE: This repository tracks only telephony-related state regarding the default mobile
+ * subscription. `TelephonyListenerManager` does not create new instances of `TelephonyManager` on a
+ * per-subscription basis and thus will always be tracking telephony information regarding
+ * `SubscriptionManager.getDefaultSubscriptionId`. See `TelephonyManager` and `SubscriptionManager`
+ * for more documentation.
+ */
+@SysUISingleton
+class TelephonyRepositoryImpl
+@Inject
+constructor(
+ private val manager: TelephonyListenerManager,
+) : TelephonyRepository {
+ @Annotation.CallState
+ override val callState: Flow<Int> = conflatedCallbackFlow {
+ val listener = TelephonyCallback.CallStateListener { state -> trySend(state) }
+
+ manager.addCallStateListener(listener)
+
+ awaitClose { manager.removeCallStateListener(listener) }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt
new file mode 100644
index 0000000..630fbf2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2022 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.telephony.data.repository
+
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface TelephonyRepositoryModule {
+ @Binds fun repository(impl: TelephonyRepositoryImpl): TelephonyRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt
new file mode 100644
index 0000000..86ca33d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2022 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.telephony.domain.interactor
+
+import android.telephony.Annotation
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.telephony.data.repository.TelephonyRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Hosts business logic related to telephony. */
+@SysUISingleton
+class TelephonyInteractor
+@Inject
+constructor(
+ repository: TelephonyRepository,
+) {
+ @Annotation.CallState val callState: Flow<Int> = repository.callState
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
index 5b522dc..0c72b78 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java
+++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
@@ -20,6 +20,7 @@
import com.android.settingslib.users.EditUserInfoController;
import com.android.systemui.user.data.repository.UserRepositoryModule;
+import com.android.systemui.user.ui.dialog.UserDialogModule;
import dagger.Binds;
import dagger.Module;
@@ -32,6 +33,7 @@
*/
@Module(
includes = {
+ UserDialogModule.class,
UserRepositoryModule.class,
}
)
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt
new file mode 100644
index 0000000..4fd55c0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 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.user.data.model
+
+/** Encapsulates the state of settings related to user switching. */
+data class UserSwitcherSettingsModel(
+ val isSimpleUserSwitcher: Boolean = false,
+ val isAddUsersFromLockscreen: Boolean = false,
+ val isUserSwitcherEnabled: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 0356388..3014f39 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -18,9 +18,13 @@
package com.android.systemui.user.data.repository
import android.content.Context
+import android.content.pm.UserInfo
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
+import android.os.UserHandle
import android.os.UserManager
+import android.provider.Settings
+import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import com.android.internal.util.UserIcons
import com.android.systemui.R
@@ -29,15 +33,36 @@
import com.android.systemui.common.shared.model.Text
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.user.data.source.UserRecord
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
/**
* Acts as source of truth for user related data.
@@ -55,6 +80,18 @@
/** List of available user-related actions. */
val actions: Flow<List<UserActionModel>>
+ /** User switcher related settings. */
+ val userSwitcherSettings: Flow<UserSwitcherSettingsModel>
+
+ /** List of all users on the device. */
+ val userInfos: Flow<List<UserInfo>>
+
+ /** [UserInfo] of the currently-selected user. */
+ val selectedUserInfo: Flow<UserInfo>
+
+ /** User ID of the last non-guest selected user. */
+ val lastSelectedNonGuestUserId: Int
+
/** Whether actions are available even when locked. */
val isActionableWhenLocked: Flow<Boolean>
@@ -62,7 +99,23 @@
val isGuestUserAutoCreated: Boolean
/** Whether the guest user is currently being reset. */
- val isGuestUserResetting: Boolean
+ var isGuestUserResetting: Boolean
+
+ /** Whether we've scheduled the creation of a guest user. */
+ val isGuestUserCreationScheduled: AtomicBoolean
+
+ /** The user of the secondary service. */
+ var secondaryUserId: Int
+
+ /** Whether refresh users should be paused. */
+ var isRefreshUsersPaused: Boolean
+
+ /** Asynchronously refresh the list of users. This will cause [userInfos] to be updated. */
+ fun refreshUsers()
+
+ fun getSelectedUserInfo(): UserInfo
+
+ fun isSimpleUserSwitcher(): Boolean
}
@SysUISingleton
@@ -71,9 +124,31 @@
constructor(
@Application private val appContext: Context,
private val manager: UserManager,
- controller: UserSwitcherController,
+ private val controller: UserSwitcherController,
+ @Application private val applicationScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val globalSettings: GlobalSettings,
+ private val tracker: UserTracker,
+ private val featureFlags: FeatureFlags,
) : UserRepository {
+ private val isNewImpl: Boolean
+ get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
+
+ private val _userSwitcherSettings = MutableStateFlow<UserSwitcherSettingsModel?>(null)
+ override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
+ _userSwitcherSettings.asStateFlow().filterNotNull()
+
+ private val _userInfos = MutableStateFlow<List<UserInfo>?>(null)
+ override val userInfos: Flow<List<UserInfo>> = _userInfos.filterNotNull()
+
+ private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
+ override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
+
+ override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
+ private set
+
private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow {
fun send() {
trySendWithFailureLogging(
@@ -99,11 +174,148 @@
override val actions: Flow<List<UserActionModel>> =
userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } }
- override val isActionableWhenLocked: Flow<Boolean> = controller.isAddUsersFromLockScreenEnabled
+ override val isActionableWhenLocked: Flow<Boolean> =
+ if (isNewImpl) {
+ emptyFlow()
+ } else {
+ controller.isAddUsersFromLockScreenEnabled
+ }
- override val isGuestUserAutoCreated: Boolean = controller.isGuestUserAutoCreated
+ override val isGuestUserAutoCreated: Boolean =
+ if (isNewImpl) {
+ appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated)
+ } else {
+ controller.isGuestUserAutoCreated
+ }
- override val isGuestUserResetting: Boolean = controller.isGuestUserResetting
+ private var _isGuestUserResetting: Boolean = false
+ override var isGuestUserResetting: Boolean =
+ if (isNewImpl) {
+ _isGuestUserResetting
+ } else {
+ controller.isGuestUserResetting
+ }
+ set(value) =
+ if (isNewImpl) {
+ _isGuestUserResetting = value
+ } else {
+ error("Not supported in the old implementation!")
+ }
+
+ override val isGuestUserCreationScheduled = AtomicBoolean()
+
+ override var secondaryUserId: Int = UserHandle.USER_NULL
+
+ override var isRefreshUsersPaused: Boolean = false
+
+ init {
+ if (isNewImpl) {
+ observeSelectedUser()
+ observeUserSettings()
+ }
+ }
+
+ override fun refreshUsers() {
+ applicationScope.launch {
+ val result = withContext(backgroundDispatcher) { manager.aliveUsers }
+
+ if (result != null) {
+ _userInfos.value = result
+ }
+ }
+ }
+
+ override fun getSelectedUserInfo(): UserInfo {
+ return checkNotNull(_selectedUserInfo.value)
+ }
+
+ override fun isSimpleUserSwitcher(): Boolean {
+ return checkNotNull(_userSwitcherSettings.value?.isSimpleUserSwitcher)
+ }
+
+ private fun observeSelectedUser() {
+ conflatedCallbackFlow {
+ fun send() {
+ trySendWithFailureLogging(tracker.userInfo, TAG)
+ }
+
+ val callback =
+ object : UserTracker.Callback {
+ override fun onUserChanged(newUser: Int, userContext: Context) {
+ send()
+ }
+ }
+
+ tracker.addCallback(callback, mainDispatcher.asExecutor())
+ send()
+
+ awaitClose { tracker.removeCallback(callback) }
+ }
+ .onEach {
+ if (!it.isGuest) {
+ lastSelectedNonGuestUserId = it.id
+ }
+
+ _selectedUserInfo.value = it
+ }
+ .launchIn(applicationScope)
+ }
+
+ private fun observeUserSettings() {
+ globalSettings
+ .observerFlow(
+ names =
+ arrayOf(
+ SETTING_SIMPLE_USER_SWITCHER,
+ Settings.Global.ADD_USERS_WHEN_LOCKED,
+ Settings.Global.USER_SWITCHER_ENABLED,
+ ),
+ userId = UserHandle.USER_SYSTEM,
+ )
+ .onStart { emit(Unit) } // Forces an initial update.
+ .map { getSettings() }
+ .onEach { _userSwitcherSettings.value = it }
+ .launchIn(applicationScope)
+ }
+
+ private suspend fun getSettings(): UserSwitcherSettingsModel {
+ return withContext(backgroundDispatcher) {
+ val isSimpleUserSwitcher =
+ globalSettings.getIntForUser(
+ SETTING_SIMPLE_USER_SWITCHER,
+ if (
+ appContext.resources.getBoolean(
+ com.android.internal.R.bool.config_expandLockScreenUserSwitcher
+ )
+ ) {
+ 1
+ } else {
+ 0
+ },
+ UserHandle.USER_SYSTEM,
+ ) != 0
+
+ val isAddUsersFromLockscreen =
+ globalSettings.getIntForUser(
+ Settings.Global.ADD_USERS_WHEN_LOCKED,
+ 0,
+ UserHandle.USER_SYSTEM,
+ ) != 0
+
+ val isUserSwitcherEnabled =
+ globalSettings.getIntForUser(
+ Settings.Global.USER_SWITCHER_ENABLED,
+ 0,
+ UserHandle.USER_SYSTEM,
+ ) != 0
+
+ UserSwitcherSettingsModel(
+ isSimpleUserSwitcher = isSimpleUserSwitcher,
+ isAddUsersFromLockscreen = isAddUsersFromLockscreen,
+ isUserSwitcherEnabled = isUserSwitcherEnabled,
+ )
+ }
+ }
private fun UserRecord.isUser(): Boolean {
return when {
@@ -125,6 +337,7 @@
image = getUserImage(this),
isSelected = isCurrent,
isSelectable = isSwitchToEnabled || isGuest,
+ isGuest = isGuest,
)
}
@@ -162,5 +375,6 @@
companion object {
private const val TAG = "UserRepository"
+ @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
index cf6da9a..9370286 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt
@@ -19,6 +19,7 @@
import android.content.pm.UserInfo
import android.graphics.Bitmap
import android.os.UserHandle
+import com.android.settingslib.RestrictedLockUtils
/** Encapsulates raw data for a user or an option item related to managing users on the device. */
data class UserRecord(
@@ -41,6 +42,11 @@
@JvmField val isSwitchToEnabled: Boolean = false,
/** Whether this record represents an option to add another supervised user to the device. */
@JvmField val isAddSupervisedUser: Boolean = false,
+ /**
+ * An enforcing admin, if the user action represented by this record is disabled by the admin.
+ * If not disabled, this is `null`.
+ */
+ @JvmField val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin? = null,
) {
/** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */
fun copyWithIsCurrent(isCurrent: Boolean): UserRecord {
@@ -59,6 +65,14 @@
}
}
+ /**
+ * Returns `true` if the user action represented by this record has been disabled by an admin;
+ * `false` otherwise.
+ */
+ fun isDisabledByAdmin(): Boolean {
+ return enforcedAdmin != null
+ }
+
companion object {
@JvmStatic
fun createForGuest(): UserRecord {
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
new file mode 100644
index 0000000..07e5cf9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2022 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.user.domain.interactor
+
+import android.annotation.UserIdInt
+import android.app.admin.DevicePolicyManager
+import android.content.Context
+import android.content.pm.UserInfo
+import android.os.RemoteException
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.Log
+import android.view.WindowManagerGlobal
+import android.widget.Toast
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.QSUserSwitcherEvent
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+/** Encapsulates business logic to interact with guest user data and systems. */
+@SysUISingleton
+class GuestUserInteractor
+@Inject
+constructor(
+ @Application private val applicationContext: Context,
+ @Application private val applicationScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val manager: UserManager,
+ private val repository: UserRepository,
+ private val deviceProvisionedController: DeviceProvisionedController,
+ private val devicePolicyManager: DevicePolicyManager,
+ private val refreshUsersScheduler: RefreshUsersScheduler,
+ private val uiEventLogger: UiEventLogger,
+) {
+ /** Whether the device is configured to always have a guest user available. */
+ val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated
+
+ /** Whether the guest user is currently being reset. */
+ val isGuestUserResetting: Boolean = repository.isGuestUserResetting
+
+ /** Notifies that the device has finished booting. */
+ fun onDeviceBootCompleted() {
+ applicationScope.launch {
+ if (isDeviceAllowedToAddGuest()) {
+ guaranteePresent()
+ return@launch
+ }
+
+ suspendCancellableCoroutine<Unit> { continuation ->
+ val callback =
+ object : DeviceProvisionedController.DeviceProvisionedListener {
+ override fun onDeviceProvisionedChanged() {
+ continuation.resumeWith(Result.success(Unit))
+ deviceProvisionedController.removeCallback(this)
+ }
+ }
+
+ deviceProvisionedController.addCallback(callback)
+ }
+
+ if (isDeviceAllowedToAddGuest()) {
+ guaranteePresent()
+ }
+ }
+ }
+
+ /** Creates a guest user and switches to it. */
+ fun createAndSwitchTo(
+ showDialog: (ShowDialogRequestModel) -> Unit,
+ dismissDialog: () -> Unit,
+ selectUser: (userId: Int) -> Unit,
+ ) {
+ applicationScope.launch {
+ val newGuestUserId = create(showDialog, dismissDialog)
+ if (newGuestUserId != UserHandle.USER_NULL) {
+ selectUser(newGuestUserId)
+ }
+ }
+ }
+
+ /** Exits the guest user, switching back to the last non-guest user or to the default user. */
+ fun exit(
+ @UserIdInt guestUserId: Int,
+ @UserIdInt targetUserId: Int,
+ forceRemoveGuestOnExit: Boolean,
+ showDialog: (ShowDialogRequestModel) -> Unit,
+ dismissDialog: () -> Unit,
+ switchUser: (userId: Int) -> Unit,
+ ) {
+ val currentUserInfo = repository.getSelectedUserInfo()
+ if (currentUserInfo.id != guestUserId) {
+ Log.w(
+ TAG,
+ "User requesting to start a new session ($guestUserId) is not current user" +
+ " (${currentUserInfo.id})"
+ )
+ return
+ }
+
+ if (!currentUserInfo.isGuest) {
+ Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
+ return
+ }
+
+ applicationScope.launch {
+ var newUserId = UserHandle.USER_SYSTEM
+ if (targetUserId == UserHandle.USER_NULL) {
+ // When a target user is not specified switch to last non guest user:
+ val lastSelectedNonGuestUserHandle = repository.lastSelectedNonGuestUserId
+ if (lastSelectedNonGuestUserHandle != UserHandle.USER_SYSTEM) {
+ val info =
+ withContext(backgroundDispatcher) {
+ manager.getUserInfo(lastSelectedNonGuestUserHandle)
+ }
+ if (info != null && info.isEnabled && info.supportsSwitchToByUser()) {
+ newUserId = info.id
+ }
+ }
+ } else {
+ newUserId = targetUserId
+ }
+
+ if (currentUserInfo.isEphemeral || forceRemoveGuestOnExit) {
+ uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE)
+ remove(currentUserInfo.id, newUserId, showDialog, dismissDialog, switchUser)
+ } else {
+ uiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH)
+ switchUser(newUserId)
+ }
+ }
+ }
+
+ /**
+ * Guarantees that the guest user is present on the device, creating it if needed and if allowed
+ * to.
+ */
+ suspend fun guaranteePresent() {
+ if (!isDeviceAllowedToAddGuest()) {
+ return
+ }
+
+ val guestUser = withContext(backgroundDispatcher) { manager.findCurrentGuestUser() }
+ if (guestUser == null) {
+ scheduleCreation()
+ }
+ }
+
+ /** Removes the guest user from the device. */
+ suspend fun remove(
+ @UserIdInt guestUserId: Int,
+ @UserIdInt targetUserId: Int,
+ showDialog: (ShowDialogRequestModel) -> Unit,
+ dismissDialog: () -> Unit,
+ switchUser: (userId: Int) -> Unit,
+ ) {
+ val currentUser: UserInfo = repository.getSelectedUserInfo()
+ if (currentUser.id != guestUserId) {
+ Log.w(
+ TAG,
+ "User requesting to start a new session ($guestUserId) is not current user" +
+ " ($currentUser.id)"
+ )
+ return
+ }
+
+ if (!currentUser.isGuest) {
+ Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest")
+ return
+ }
+
+ val marked =
+ withContext(backgroundDispatcher) { manager.markGuestForDeletion(currentUser.id) }
+ if (!marked) {
+ Log.w(TAG, "Couldn't mark the guest for deletion for user $guestUserId")
+ return
+ }
+
+ if (targetUserId == UserHandle.USER_NULL) {
+ // Create a new guest in the foreground, and then immediately switch to it
+ val newGuestId = create(showDialog, dismissDialog)
+ if (newGuestId == UserHandle.USER_NULL) {
+ Log.e(TAG, "Could not create new guest, switching back to system user")
+ switchUser(UserHandle.USER_SYSTEM)
+ withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) }
+ try {
+ WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null)
+ } catch (e: RemoteException) {
+ Log.e(
+ TAG,
+ "Couldn't remove guest because ActivityManager or WindowManager is dead"
+ )
+ }
+ return
+ }
+
+ switchUser(newGuestId)
+
+ withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) }
+ } else {
+ if (repository.isGuestUserAutoCreated) {
+ repository.isGuestUserResetting = true
+ }
+ switchUser(targetUserId)
+ manager.removeUser(currentUser.id)
+ }
+ }
+
+ /**
+ * Creates the guest user and adds it to the device.
+ *
+ * @param showDialog A function to invoke to show a dialog.
+ * @param dismissDialog A function to invoke to dismiss a dialog.
+ * @return The user ID of the newly-created guest user.
+ */
+ private suspend fun create(
+ showDialog: (ShowDialogRequestModel) -> Unit,
+ dismissDialog: () -> Unit,
+ ): Int {
+ return withContext(mainDispatcher) {
+ showDialog(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
+ val guestUserId = createInBackground()
+ dismissDialog()
+ if (guestUserId != UserHandle.USER_NULL) {
+ uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD)
+ } else {
+ Toast.makeText(
+ applicationContext,
+ com.android.settingslib.R.string.add_guest_failed,
+ Toast.LENGTH_SHORT,
+ )
+ .show()
+ }
+
+ guestUserId
+ }
+ }
+
+ /** Schedules the creation of the guest user. */
+ private suspend fun scheduleCreation() {
+ if (!repository.isGuestUserCreationScheduled.compareAndSet(false, true)) {
+ return
+ }
+
+ withContext(backgroundDispatcher) {
+ val newGuestUserId = createInBackground()
+ repository.isGuestUserCreationScheduled.set(false)
+ repository.isGuestUserResetting = false
+ if (newGuestUserId == UserHandle.USER_NULL) {
+ Log.w(TAG, "Could not create new guest while exiting existing guest")
+ // Refresh users so that we still display "Guest" if
+ // config_guestUserAutoCreated=true
+ refreshUsersScheduler.refreshIfNotPaused()
+ }
+ }
+ }
+
+ /**
+ * Creates a guest user and return its multi-user user ID.
+ *
+ * This method does not check if a guest already exists before it makes a call to [UserManager]
+ * to create a new one.
+ *
+ * @return The multi-user user ID of the newly created guest user, or [UserHandle.USER_NULL] if
+ * the guest couldn't be created.
+ */
+ @UserIdInt
+ private suspend fun createInBackground(): Int {
+ return withContext(backgroundDispatcher) {
+ try {
+ val guestUser = manager.createGuest(applicationContext)
+ if (guestUser != null) {
+ guestUser.id
+ } else {
+ Log.e(
+ TAG,
+ "Couldn't create guest, most likely because there already exists one!"
+ )
+ UserHandle.USER_NULL
+ }
+ } catch (e: UserManager.UserOperationException) {
+ Log.e(TAG, "Couldn't create guest user!", e)
+ UserHandle.USER_NULL
+ }
+ }
+ }
+
+ private fun isDeviceAllowedToAddGuest(): Boolean {
+ return deviceProvisionedController.isDeviceProvisioned &&
+ !devicePolicyManager.isDeviceManaged
+ }
+
+ companion object {
+ private const val TAG = "GuestUserInteractor"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt
new file mode 100644
index 0000000..8f36821
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2022 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.user.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/** Encapsulates logic for pausing, unpausing, and scheduling a delayed job. */
+@SysUISingleton
+class RefreshUsersScheduler
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ private val repository: UserRepository,
+) {
+ private var scheduledUnpauseJob: Job? = null
+ private var isPaused = false
+
+ fun pause() {
+ applicationScope.launch(mainDispatcher) {
+ isPaused = true
+ scheduledUnpauseJob?.cancel()
+ scheduledUnpauseJob =
+ applicationScope.launch {
+ delay(PAUSE_REFRESH_USERS_TIMEOUT_MS)
+ unpauseAndRefresh()
+ }
+ }
+ }
+
+ fun unpauseAndRefresh() {
+ applicationScope.launch(mainDispatcher) {
+ isPaused = false
+ refreshIfNotPaused()
+ }
+ }
+
+ fun refreshIfNotPaused() {
+ applicationScope.launch(mainDispatcher) {
+ if (isPaused) {
+ return@launch
+ }
+
+ repository.refreshUsers()
+ }
+ }
+
+ companion object {
+ private const val PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000L
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
new file mode 100644
index 0000000..1b4746a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2022 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.user.domain.interactor
+
+import android.os.UserHandle
+import android.os.UserManager
+import com.android.systemui.user.data.repository.UserRepository
+
+/** Utilities related to user management actions. */
+object UserActionsUtil {
+
+ /** Returns `true` if it's possible to add a guest user to the device; `false` otherwise. */
+ fun canCreateGuest(
+ manager: UserManager,
+ repository: UserRepository,
+ isUserSwitcherEnabled: Boolean,
+ isAddUsersFromLockScreenEnabled: Boolean,
+ ): Boolean {
+ if (!isUserSwitcherEnabled) {
+ return false
+ }
+
+ return currentUserCanCreateUsers(manager, repository) ||
+ anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled)
+ }
+
+ /** Returns `true` if it's possible to add a user to the device; `false` otherwise. */
+ fun canCreateUser(
+ manager: UserManager,
+ repository: UserRepository,
+ isUserSwitcherEnabled: Boolean,
+ isAddUsersFromLockScreenEnabled: Boolean,
+ ): Boolean {
+ if (!isUserSwitcherEnabled) {
+ return false
+ }
+
+ if (
+ !currentUserCanCreateUsers(manager, repository) &&
+ !anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled)
+ ) {
+ return false
+ }
+
+ return manager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY)
+ }
+
+ /**
+ * Returns `true` if it's possible to add a supervised user to the device; `false` otherwise.
+ */
+ fun canCreateSupervisedUser(
+ manager: UserManager,
+ repository: UserRepository,
+ isUserSwitcherEnabled: Boolean,
+ isAddUsersFromLockScreenEnabled: Boolean,
+ supervisedUserPackageName: String?
+ ): Boolean {
+ if (supervisedUserPackageName.isNullOrEmpty()) {
+ return false
+ }
+
+ return canCreateUser(
+ manager,
+ repository,
+ isUserSwitcherEnabled,
+ isAddUsersFromLockScreenEnabled
+ )
+ }
+
+ /**
+ * Returns `true` if the current user is allowed to add users to the device; `false` otherwise.
+ */
+ private fun currentUserCanCreateUsers(
+ manager: UserManager,
+ repository: UserRepository,
+ ): Boolean {
+ val currentUser = repository.getSelectedUserInfo()
+ if (!currentUser.isAdmin && currentUser.id != UserHandle.USER_SYSTEM) {
+ return false
+ }
+
+ return systemCanCreateUsers(manager)
+ }
+
+ /** Returns `true` if the system can add users to the device; `false` otherwise. */
+ private fun systemCanCreateUsers(
+ manager: UserManager,
+ ): Boolean {
+ return !manager.hasBaseUserRestriction(UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM)
+ }
+
+ /** Returns `true` if it's allowed to add users to the device at all; `false` otherwise. */
+ private fun anyoneCanCreateUsers(
+ manager: UserManager,
+ isAddUsersFromLockScreenEnabled: Boolean,
+ ): Boolean {
+ return systemCanCreateUsers(manager) && isAddUsersFromLockScreenEnabled
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
index 3c5b969..a84238c 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt
@@ -17,94 +17,725 @@
package com.android.systemui.user.domain.interactor
+import android.annotation.SuppressLint
+import android.annotation.UserIdInt
+import android.app.ActivityManager
+import android.content.Context
import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.UserInfo
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.os.RemoteException
+import android.os.UserHandle
+import android.os.UserManager
import android.provider.Settings
+import android.util.Log
+import com.android.internal.util.UserIcons
+import com.android.systemui.R
+import com.android.systemui.SystemUISecondaryUserService
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.shared.model.Text
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.kotlin.pairwise
+import java.io.PrintWriter
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
/** Encapsulates business logic to interact with user data and systems. */
@SysUISingleton
class UserInteractor
@Inject
constructor(
- repository: UserRepository,
+ @Application private val applicationContext: Context,
+ private val repository: UserRepository,
private val controller: UserSwitcherController,
private val activityStarter: ActivityStarter,
- keyguardInteractor: KeyguardInteractor,
+ private val keyguardInteractor: KeyguardInteractor,
+ private val featureFlags: FeatureFlags,
+ private val manager: UserManager,
+ @Application private val applicationScope: CoroutineScope,
+ telephonyInteractor: TelephonyInteractor,
+ broadcastDispatcher: BroadcastDispatcher,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val activityManager: ActivityManager,
+ private val refreshUsersScheduler: RefreshUsersScheduler,
+ private val guestUserInteractor: GuestUserInteractor,
) {
+ /**
+ * Defines interface for classes that can be notified when the state of users on the device is
+ * changed.
+ */
+ interface UserCallback {
+ /** Returns `true` if this callback can be cleaned-up. */
+ fun isEvictable(): Boolean = false
+ /** Notifies that the state of users on the device has changed. */
+ fun onUserStateChanged()
+ }
+
+ private val isNewImpl: Boolean
+ get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
+
+ private val supervisedUserPackageName: String?
+ get() =
+ applicationContext.getString(
+ com.android.internal.R.string.config_supervisedUserCreationPackage
+ )
+
+ private val callbackMutex = Mutex()
+ private val callbacks = mutableSetOf<UserCallback>()
+
/** List of current on-device users to select from. */
- val users: Flow<List<UserModel>> = repository.users
+ val users: Flow<List<UserModel>>
+ get() =
+ if (isNewImpl) {
+ combine(
+ repository.userInfos,
+ repository.selectedUserInfo,
+ repository.userSwitcherSettings,
+ ) { userInfos, selectedUserInfo, settings ->
+ toUserModels(
+ userInfos = userInfos,
+ selectedUserId = selectedUserInfo.id,
+ isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
+ )
+ }
+ } else {
+ repository.users
+ }
/** The currently-selected user. */
- val selectedUser: Flow<UserModel> = repository.selectedUser
+ val selectedUser: Flow<UserModel>
+ get() =
+ if (isNewImpl) {
+ combine(
+ repository.selectedUserInfo,
+ repository.userSwitcherSettings,
+ ) { selectedUserInfo, settings ->
+ val selectedUserId = selectedUserInfo.id
+ checkNotNull(
+ toUserModel(
+ userInfo = selectedUserInfo,
+ selectedUserId = selectedUserId,
+ canSwitchUsers = canSwitchUsers(selectedUserId),
+ isUserSwitcherEnabled = settings.isUserSwitcherEnabled,
+ )
+ )
+ }
+ } else {
+ repository.selectedUser
+ }
/** List of user-switcher related actions that are available. */
- val actions: Flow<List<UserActionModel>> =
- combine(
- repository.isActionableWhenLocked,
- keyguardInteractor.isKeyguardShowing,
- ) { isActionableWhenLocked, isLocked ->
- isActionableWhenLocked || !isLocked
- }
- .flatMapLatest { isActionable ->
- if (isActionable) {
- repository.actions.map { actions ->
- actions +
- if (actions.isNotEmpty()) {
- // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because
- // that's a user
- // switcher specific action that is not known to the our data source
- // or other
- // features.
- listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
- } else {
- // If no actions, don't add the navigate action.
- emptyList()
- }
+ val actions: Flow<List<UserActionModel>>
+ get() =
+ if (isNewImpl) {
+ combine(
+ repository.userInfos,
+ repository.userSwitcherSettings,
+ keyguardInteractor.isKeyguardShowing,
+ ) { userInfos, settings, isDeviceLocked ->
+ buildList {
+ val hasGuestUser = userInfos.any { it.isGuest }
+ if (
+ !hasGuestUser &&
+ (guestUserInteractor.isGuestUserAutoCreated ||
+ UserActionsUtil.canCreateGuest(
+ manager,
+ repository,
+ settings.isUserSwitcherEnabled,
+ settings.isAddUsersFromLockscreen,
+ ))
+ ) {
+ add(UserActionModel.ENTER_GUEST_MODE)
+ }
+
+ if (isDeviceLocked && !settings.isAddUsersFromLockscreen) {
+ // The device is locked and our setting to allow actions that add users
+ // from the lock-screen is not enabled. The guest action from above is
+ // always allowed, even when the device is locked, but the various "add
+ // user" actions below are not. We can finish building the list here.
+ return@buildList
+ }
+
+ if (
+ UserActionsUtil.canCreateUser(
+ manager,
+ repository,
+ settings.isUserSwitcherEnabled,
+ settings.isAddUsersFromLockscreen,
+ )
+ ) {
+ add(UserActionModel.ADD_USER)
+ }
+
+ if (
+ UserActionsUtil.canCreateSupervisedUser(
+ manager,
+ repository,
+ settings.isUserSwitcherEnabled,
+ settings.isAddUsersFromLockscreen,
+ supervisedUserPackageName,
+ )
+ ) {
+ add(UserActionModel.ADD_SUPERVISED_USER)
+ }
}
- } else {
- // If not actionable it means that we're not allowed to show actions when locked
- // and we
- // are locked. Therefore, we should show no actions.
- flowOf(emptyList())
}
+ } else {
+ combine(
+ repository.isActionableWhenLocked,
+ keyguardInteractor.isKeyguardShowing,
+ ) { isActionableWhenLocked, isLocked ->
+ isActionableWhenLocked || !isLocked
+ }
+ .flatMapLatest { isActionable ->
+ if (isActionable) {
+ repository.actions.map { actions ->
+ actions +
+ if (actions.isNotEmpty()) {
+ // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT
+ // because that's a user switcher specific action that is
+ // not known to the our data source or other features.
+ listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+ } else {
+ // If no actions, don't add the navigate action.
+ emptyList()
+ }
+ }
+ } else {
+ // If not actionable it means that we're not allowed to show actions
+ // when
+ // locked and we are locked. Therefore, we should show no actions.
+ flowOf(emptyList())
+ }
+ }
}
+ val userRecords: StateFlow<ArrayList<UserRecord>> =
+ if (isNewImpl) {
+ combine(
+ repository.userInfos,
+ repository.selectedUserInfo,
+ actions,
+ repository.userSwitcherSettings,
+ ) { userInfos, selectedUserInfo, actionModels, settings ->
+ ArrayList(
+ userInfos.map {
+ toRecord(
+ userInfo = it,
+ selectedUserId = selectedUserInfo.id,
+ )
+ } +
+ actionModels.map {
+ toRecord(
+ action = it,
+ selectedUserId = selectedUserInfo.id,
+ isAddFromLockscreenEnabled = settings.isAddUsersFromLockscreen,
+ )
+ }
+ )
+ }
+ .onEach { notifyCallbacks() }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = ArrayList(),
+ )
+ } else {
+ MutableStateFlow(ArrayList())
+ }
+
+ val selectedUserRecord: StateFlow<UserRecord?> =
+ if (isNewImpl) {
+ repository.selectedUserInfo
+ .map { selectedUserInfo ->
+ toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id)
+ }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = null,
+ )
+ } else {
+ MutableStateFlow(null)
+ }
+
/** Whether the device is configured to always have a guest user available. */
- val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated
+ val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated
/** Whether the guest user is currently being reset. */
- val isGuestUserResetting: Boolean = repository.isGuestUserResetting
+ val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting
+
+ private val _dialogShowRequests = MutableStateFlow<ShowDialogRequestModel?>(null)
+ val dialogShowRequests: Flow<ShowDialogRequestModel?> = _dialogShowRequests.asStateFlow()
+
+ private val _dialogDismissRequests = MutableStateFlow<Unit?>(null)
+ val dialogDismissRequests: Flow<Unit?> = _dialogDismissRequests.asStateFlow()
+
+ val isSimpleUserSwitcher: Boolean
+ get() =
+ if (isNewImpl) {
+ repository.isSimpleUserSwitcher()
+ } else {
+ error("Not supported in the old implementation!")
+ }
+
+ init {
+ if (isNewImpl) {
+ refreshUsersScheduler.refreshIfNotPaused()
+ telephonyInteractor.callState
+ .distinctUntilChanged()
+ .onEach { refreshUsersScheduler.refreshIfNotPaused() }
+ .launchIn(applicationScope)
+
+ combine(
+ broadcastDispatcher.broadcastFlow(
+ filter =
+ IntentFilter().apply {
+ addAction(Intent.ACTION_USER_ADDED)
+ addAction(Intent.ACTION_USER_REMOVED)
+ addAction(Intent.ACTION_USER_INFO_CHANGED)
+ addAction(Intent.ACTION_USER_SWITCHED)
+ addAction(Intent.ACTION_USER_STOPPED)
+ addAction(Intent.ACTION_USER_UNLOCKED)
+ },
+ user = UserHandle.SYSTEM,
+ map = { intent, _ -> intent },
+ ),
+ repository.selectedUserInfo.pairwise(null),
+ ) { intent, selectedUserChange ->
+ Pair(intent, selectedUserChange.previousValue)
+ }
+ .onEach { (intent, previousSelectedUser) ->
+ onBroadcastReceived(intent, previousSelectedUser)
+ }
+ .launchIn(applicationScope)
+ }
+ }
+
+ fun addCallback(callback: UserCallback) {
+ applicationScope.launch { callbackMutex.withLock { callbacks.add(callback) } }
+ }
+
+ fun removeCallback(callback: UserCallback) {
+ applicationScope.launch { callbackMutex.withLock { callbacks.remove(callback) } }
+ }
+
+ fun refreshUsers() {
+ refreshUsersScheduler.refreshIfNotPaused()
+ }
+
+ fun onDialogShown() {
+ _dialogShowRequests.value = null
+ }
+
+ fun onDialogDismissed() {
+ _dialogDismissRequests.value = null
+ }
+
+ fun dump(pw: PrintWriter) {
+ pw.println("UserInteractor state:")
+ pw.println(" lastSelectedNonGuestUserId=${repository.lastSelectedNonGuestUserId}")
+
+ val users = userRecords.value.filter { it.info != null }
+ pw.println(" userCount=${userRecords.value.count { LegacyUserDataHelper.isUser(it) }}")
+ for (i in users.indices) {
+ pw.println(" ${users[i]}")
+ }
+
+ val actions = userRecords.value.filter { it.info == null }
+ pw.println(" actionCount=${userRecords.value.count { !LegacyUserDataHelper.isUser(it) }}")
+ for (i in actions.indices) {
+ pw.println(" ${actions[i]}")
+ }
+
+ pw.println("isSimpleUserSwitcher=$isSimpleUserSwitcher")
+ pw.println("isGuestUserAutoCreated=$isGuestUserAutoCreated")
+ }
+
+ fun onDeviceBootCompleted() {
+ guestUserInteractor.onDeviceBootCompleted()
+ }
/** Switches to the user with the given user ID. */
fun selectUser(
- userId: Int,
+ newlySelectedUserId: Int,
) {
- controller.onUserSelected(userId, /* dialogShower= */ null)
+ if (isNewImpl) {
+ val currentlySelectedUserInfo = repository.getSelectedUserInfo()
+ if (
+ newlySelectedUserId == currentlySelectedUserInfo.id &&
+ currentlySelectedUserInfo.isGuest
+ ) {
+ // Here when clicking on the currently-selected guest user to leave guest mode
+ // and return to the previously-selected non-guest user.
+ showDialog(
+ ShowDialogRequestModel.ShowExitGuestDialog(
+ guestUserId = currentlySelectedUserInfo.id,
+ targetUserId = repository.lastSelectedNonGuestUserId,
+ isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
+ isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+ onExitGuestUser = this::exitGuestUser,
+ )
+ )
+ return
+ }
+
+ if (currentlySelectedUserInfo.isGuest) {
+ // Here when switching from guest to a non-guest user.
+ showDialog(
+ ShowDialogRequestModel.ShowExitGuestDialog(
+ guestUserId = currentlySelectedUserInfo.id,
+ targetUserId = newlySelectedUserId,
+ isGuestEphemeral = currentlySelectedUserInfo.isEphemeral,
+ isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+ onExitGuestUser = this::exitGuestUser,
+ )
+ )
+ return
+ }
+
+ switchUser(newlySelectedUserId)
+ } else {
+ controller.onUserSelected(newlySelectedUserId, /* dialogShower= */ null)
+ }
}
/** Executes the given action. */
fun executeAction(action: UserActionModel) {
- when (action) {
- UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null)
- UserActionModel.ADD_USER -> controller.showAddUserDialog(null)
- UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity()
- UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
- activityStarter.startActivity(
- Intent(Settings.ACTION_USER_SETTINGS),
- /* dismissShade= */ false,
- )
+ if (isNewImpl) {
+ when (action) {
+ UserActionModel.ENTER_GUEST_MODE ->
+ guestUserInteractor.createAndSwitchTo(
+ this::showDialog,
+ this::dismissDialog,
+ this::selectUser,
+ )
+ UserActionModel.ADD_USER -> {
+ val currentUser = repository.getSelectedUserInfo()
+ showDialog(
+ ShowDialogRequestModel.ShowAddUserDialog(
+ userHandle = currentUser.userHandle,
+ isKeyguardShowing = keyguardInteractor.isKeyguardShowing(),
+ showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral,
+ )
+ )
+ }
+ UserActionModel.ADD_SUPERVISED_USER ->
+ activityStarter.startActivity(
+ Intent()
+ .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
+ .setPackage(supervisedUserPackageName)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+ /* dismissShade= */ false,
+ )
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
+ activityStarter.startActivity(
+ Intent(Settings.ACTION_USER_SETTINGS),
+ /* dismissShade= */ false,
+ )
+ }
+ } else {
+ when (action) {
+ UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null)
+ UserActionModel.ADD_USER -> controller.showAddUserDialog(null)
+ UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity()
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT ->
+ activityStarter.startActivity(
+ Intent(Settings.ACTION_USER_SETTINGS),
+ /* dismissShade= */ false,
+ )
+ }
}
}
+
+ fun exitGuestUser(
+ @UserIdInt guestUserId: Int,
+ @UserIdInt targetUserId: Int,
+ forceRemoveGuestOnExit: Boolean,
+ ) {
+ guestUserInteractor.exit(
+ guestUserId = guestUserId,
+ targetUserId = targetUserId,
+ forceRemoveGuestOnExit = forceRemoveGuestOnExit,
+ showDialog = this::showDialog,
+ dismissDialog = this::dismissDialog,
+ switchUser = this::switchUser,
+ )
+ }
+
+ fun removeGuestUser(
+ @UserIdInt guestUserId: Int,
+ @UserIdInt targetUserId: Int,
+ ) {
+ applicationScope.launch {
+ guestUserInteractor.remove(
+ guestUserId = guestUserId,
+ targetUserId = targetUserId,
+ ::showDialog,
+ ::dismissDialog,
+ ::selectUser,
+ )
+ }
+ }
+
+ private fun showDialog(request: ShowDialogRequestModel) {
+ _dialogShowRequests.value = request
+ }
+
+ private fun dismissDialog() {
+ _dialogDismissRequests.value = Unit
+ }
+
+ private fun notifyCallbacks() {
+ applicationScope.launch {
+ callbackMutex.withLock {
+ val iterator = callbacks.iterator()
+ while (iterator.hasNext()) {
+ val callback = iterator.next()
+ if (!callback.isEvictable()) {
+ callback.onUserStateChanged()
+ } else {
+ iterator.remove()
+ }
+ }
+ }
+ }
+ }
+
+ private suspend fun toRecord(
+ userInfo: UserInfo,
+ selectedUserId: Int,
+ ): UserRecord {
+ return LegacyUserDataHelper.createRecord(
+ context = applicationContext,
+ manager = manager,
+ userInfo = userInfo,
+ picture = null,
+ isCurrent = userInfo.id == selectedUserId,
+ canSwitchUsers = canSwitchUsers(selectedUserId),
+ )
+ }
+
+ private suspend fun toRecord(
+ action: UserActionModel,
+ selectedUserId: Int,
+ isAddFromLockscreenEnabled: Boolean,
+ ): UserRecord {
+ return LegacyUserDataHelper.createRecord(
+ context = applicationContext,
+ selectedUserId = selectedUserId,
+ actionType = action,
+ isRestricted =
+ if (action == UserActionModel.ENTER_GUEST_MODE) {
+ // Entering guest mode is never restricted, so it's allowed to happen from the
+ // lockscreen even if the "add from lockscreen" system setting is off.
+ false
+ } else {
+ !isAddFromLockscreenEnabled
+ },
+ isSwitchToEnabled =
+ canSwitchUsers(selectedUserId) &&
+ // If the user is auto-created is must not be currently resetting.
+ !(isGuestUserAutoCreated && isGuestUserResetting),
+ )
+ }
+
+ private fun switchUser(userId: Int) {
+ // TODO(b/246631653): track jank and lantecy like in the old impl.
+ refreshUsersScheduler.pause()
+ try {
+ activityManager.switchUser(userId)
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Couldn't switch user.", e)
+ }
+ }
+
+ private suspend fun onBroadcastReceived(
+ intent: Intent,
+ previousUserInfo: UserInfo?,
+ ) {
+ val shouldRefreshAllUsers =
+ when (intent.action) {
+ Intent.ACTION_USER_SWITCHED -> {
+ dismissDialog()
+ val selectedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
+ if (previousUserInfo?.id != selectedUserId) {
+ notifyCallbacks()
+ restartSecondaryService(selectedUserId)
+ }
+ if (guestUserInteractor.isGuestUserAutoCreated) {
+ guestUserInteractor.guaranteePresent()
+ }
+ true
+ }
+ Intent.ACTION_USER_INFO_CHANGED -> true
+ Intent.ACTION_USER_UNLOCKED -> {
+ // If we unlocked the system user, we should refresh all users.
+ intent.getIntExtra(
+ Intent.EXTRA_USER_HANDLE,
+ UserHandle.USER_NULL,
+ ) == UserHandle.USER_SYSTEM
+ }
+ else -> true
+ }
+
+ if (shouldRefreshAllUsers) {
+ refreshUsersScheduler.unpauseAndRefresh()
+ }
+ }
+
+ private fun restartSecondaryService(@UserIdInt userId: Int) {
+ val intent = Intent(applicationContext, SystemUISecondaryUserService::class.java)
+ // Disconnect from the old secondary user's service
+ val secondaryUserId = repository.secondaryUserId
+ if (secondaryUserId != UserHandle.USER_NULL) {
+ applicationContext.stopServiceAsUser(
+ intent,
+ UserHandle.of(secondaryUserId),
+ )
+ repository.secondaryUserId = UserHandle.USER_NULL
+ }
+
+ // Connect to the new secondary user's service (purely to ensure that a persistent
+ // SystemUI application is created for that user)
+ if (userId != UserHandle.USER_SYSTEM) {
+ applicationContext.startServiceAsUser(
+ intent,
+ UserHandle.of(userId),
+ )
+ repository.secondaryUserId = userId
+ }
+ }
+
+ private suspend fun toUserModels(
+ userInfos: List<UserInfo>,
+ selectedUserId: Int,
+ isUserSwitcherEnabled: Boolean,
+ ): List<UserModel> {
+ val canSwitchUsers = canSwitchUsers(selectedUserId)
+
+ return userInfos
+ // The guest user should go in the last position.
+ .sortedBy { it.isGuest }
+ .mapNotNull { userInfo ->
+ toUserModel(
+ userInfo = userInfo,
+ selectedUserId = selectedUserId,
+ canSwitchUsers = canSwitchUsers,
+ isUserSwitcherEnabled = isUserSwitcherEnabled,
+ )
+ }
+ }
+
+ private suspend fun toUserModel(
+ userInfo: UserInfo,
+ selectedUserId: Int,
+ canSwitchUsers: Boolean,
+ isUserSwitcherEnabled: Boolean,
+ ): UserModel? {
+ val userId = userInfo.id
+ val isSelected = userId == selectedUserId
+
+ return when {
+ // When the user switcher is not enabled in settings, we only show the primary user.
+ !isUserSwitcherEnabled && !userInfo.isPrimary -> null
+
+ // We avoid showing disabled users.
+ !userInfo.isEnabled -> null
+ userInfo.isGuest ->
+ UserModel(
+ id = userId,
+ name = Text.Loaded(userInfo.name),
+ image =
+ getUserImage(
+ isGuest = true,
+ userId = userId,
+ ),
+ isSelected = isSelected,
+ isSelectable = canSwitchUsers,
+ isGuest = true,
+ )
+ userInfo.supportsSwitchToByUser() ->
+ UserModel(
+ id = userId,
+ name = Text.Loaded(userInfo.name),
+ image =
+ getUserImage(
+ isGuest = false,
+ userId = userId,
+ ),
+ isSelected = isSelected,
+ isSelectable = canSwitchUsers || isSelected,
+ isGuest = false,
+ )
+ else -> null
+ }
+ }
+
+ private suspend fun canSwitchUsers(selectedUserId: Int): Boolean {
+ return withContext(backgroundDispatcher) {
+ manager.getUserSwitchability(UserHandle.of(selectedUserId))
+ } == UserManager.SWITCHABILITY_STATUS_OK
+ }
+
+ @SuppressLint("UseCompatLoadingForDrawables")
+ private suspend fun getUserImage(
+ isGuest: Boolean,
+ userId: Int,
+ ): Drawable {
+ if (isGuest) {
+ return checkNotNull(applicationContext.getDrawable(R.drawable.ic_account_circle))
+ }
+
+ // TODO(b/246631653): cache the bitmaps to avoid the background work to fetch them.
+ // TODO(b/246631653): downscale the bitmaps to R.dimen.max_avatar_size if requested.
+ val userIcon = withContext(backgroundDispatcher) { manager.getUserIcon(userId) }
+ if (userIcon != null) {
+ return BitmapDrawable(userIcon)
+ }
+
+ return UserIcons.getDefaultUserIcon(
+ applicationContext.resources,
+ userId,
+ /* light= */ false
+ )
+ }
+
+ companion object {
+ private const val TAG = "UserInteractor"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
new file mode 100644
index 0000000..08d7c5a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2022 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.user.domain.model
+
+import android.os.UserHandle
+
+/** Encapsulates a request to show a dialog. */
+sealed class ShowDialogRequestModel {
+ data class ShowAddUserDialog(
+ val userHandle: UserHandle,
+ val isKeyguardShowing: Boolean,
+ val showEphemeralMessage: Boolean,
+ ) : ShowDialogRequestModel()
+
+ data class ShowUserCreationDialog(
+ val isGuest: Boolean,
+ ) : ShowDialogRequestModel()
+
+ data class ShowExitGuestDialog(
+ val guestUserId: Int,
+ val targetUserId: Int,
+ val isGuestEphemeral: Boolean,
+ val isKeyguardShowing: Boolean,
+ val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit,
+ ) : ShowDialogRequestModel()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
new file mode 100644
index 0000000..137de15
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2022 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.user.legacyhelper.data
+
+import android.content.Context
+import android.content.pm.UserInfo
+import android.graphics.Bitmap
+import android.os.UserManager
+import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
+import com.android.settingslib.RestrictedLockUtilsInternal
+import com.android.systemui.R
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.shared.model.UserActionModel
+
+/**
+ * Defines utility functions for helping with legacy data code for users.
+ *
+ * We need these to avoid code duplication between logic inside the UserSwitcherController and in
+ * modern architecture classes such as repositories, interactors, and view-models. If we ever
+ * simplify UserSwitcherController (or delete it), the code here could be moved into its call-sites.
+ */
+object LegacyUserDataHelper {
+
+ @JvmStatic
+ fun createRecord(
+ context: Context,
+ manager: UserManager,
+ picture: Bitmap?,
+ userInfo: UserInfo,
+ isCurrent: Boolean,
+ canSwitchUsers: Boolean,
+ ): UserRecord {
+ val isGuest = userInfo.isGuest
+ return UserRecord(
+ info = userInfo,
+ picture =
+ getPicture(
+ manager = manager,
+ context = context,
+ userInfo = userInfo,
+ picture = picture,
+ ),
+ isGuest = isGuest,
+ isCurrent = isCurrent,
+ isSwitchToEnabled = canSwitchUsers || (isCurrent && !isGuest),
+ )
+ }
+
+ @JvmStatic
+ fun createRecord(
+ context: Context,
+ selectedUserId: Int,
+ actionType: UserActionModel,
+ isRestricted: Boolean,
+ isSwitchToEnabled: Boolean,
+ ): UserRecord {
+ return UserRecord(
+ isGuest = actionType == UserActionModel.ENTER_GUEST_MODE,
+ isAddUser = actionType == UserActionModel.ADD_USER,
+ isAddSupervisedUser = actionType == UserActionModel.ADD_SUPERVISED_USER,
+ isRestricted = isRestricted,
+ isSwitchToEnabled = isSwitchToEnabled,
+ enforcedAdmin =
+ getEnforcedAdmin(
+ context = context,
+ selectedUserId = selectedUserId,
+ ),
+ )
+ }
+
+ fun toUserActionModel(record: UserRecord): UserActionModel {
+ check(!isUser(record))
+
+ return when {
+ record.isAddUser -> UserActionModel.ADD_USER
+ record.isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER
+ record.isGuest -> UserActionModel.ENTER_GUEST_MODE
+ else -> error("Not a known action: $record")
+ }
+ }
+
+ fun isUser(record: UserRecord): Boolean {
+ return record.info != null
+ }
+
+ private fun getEnforcedAdmin(
+ context: Context,
+ selectedUserId: Int,
+ ): EnforcedAdmin? {
+ val admin =
+ RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
+ context,
+ UserManager.DISALLOW_ADD_USER,
+ selectedUserId,
+ )
+ ?: return null
+
+ return if (
+ !RestrictedLockUtilsInternal.hasBaseUserRestriction(
+ context,
+ UserManager.DISALLOW_ADD_USER,
+ selectedUserId,
+ )
+ ) {
+ admin
+ } else {
+ null
+ }
+ }
+
+ private fun getPicture(
+ context: Context,
+ manager: UserManager,
+ userInfo: UserInfo,
+ picture: Bitmap?,
+ ): Bitmap? {
+ if (userInfo.isGuest) {
+ return null
+ }
+
+ if (picture != null) {
+ return picture
+ }
+
+ val unscaledOrNull = manager.getUserIcon(userInfo.id) ?: return null
+
+ val avatarSize = context.resources.getDimensionPixelSize(R.dimen.max_avatar_size)
+ return Bitmap.createScaledBitmap(
+ unscaledOrNull,
+ avatarSize,
+ avatarSize,
+ /* filter= */ true,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt
index bf7977a..2095683 100644
--- a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt
@@ -32,4 +32,6 @@
val isSelected: Boolean,
/** Whether this use is selectable. A non-selectable user cannot be switched to. */
val isSelectable: Boolean,
+ /** Whether this model represents the guest user. */
+ val isGuest: Boolean,
)
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt
new file mode 100644
index 0000000..a9d66de
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 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.user.ui.dialog
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.UserHandle
+import com.android.settingslib.R
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.user.CreateUserActivity
+
+/** Dialog for adding a new user to the device. */
+class AddUserDialog(
+ context: Context,
+ userHandle: UserHandle,
+ isKeyguardShowing: Boolean,
+ showEphemeralMessage: Boolean,
+ private val falsingManager: FalsingManager,
+ private val broadcastSender: BroadcastSender,
+ private val dialogLaunchAnimator: DialogLaunchAnimator
+) : SystemUIDialog(context) {
+
+ private val onClickListener =
+ object : DialogInterface.OnClickListener {
+ override fun onClick(dialog: DialogInterface, which: Int) {
+ val penalty =
+ if (which == BUTTON_NEGATIVE) {
+ FalsingManager.NO_PENALTY
+ } else {
+ FalsingManager.MODERATE_PENALTY
+ }
+ if (falsingManager.isFalseTap(penalty)) {
+ return
+ }
+
+ if (which == BUTTON_NEUTRAL) {
+ cancel()
+ return
+ }
+
+ dialogLaunchAnimator.dismissStack(this@AddUserDialog)
+ if (ActivityManager.isUserAMonkey()) {
+ return
+ }
+
+ // Use broadcast instead of ShadeController, as this dialog may have started in
+ // another
+ // process where normal dagger bindings are not available.
+ broadcastSender.sendBroadcastAsUser(
+ Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS),
+ UserHandle.CURRENT
+ )
+
+ context.startActivityAsUser(
+ CreateUserActivity.createIntentForStart(context),
+ userHandle,
+ )
+ }
+ }
+
+ init {
+ setTitle(R.string.user_add_user_title)
+ val message =
+ context.getString(R.string.user_add_user_message_short) +
+ if (showEphemeralMessage) {
+ context.getString(
+ com.android.systemui.R.string.user_add_user_message_guest_remove
+ )
+ } else {
+ ""
+ }
+ setMessage(message)
+
+ setButton(
+ BUTTON_NEUTRAL,
+ context.getString(android.R.string.cancel),
+ onClickListener,
+ )
+
+ setButton(
+ BUTTON_POSITIVE,
+ context.getString(android.R.string.ok),
+ onClickListener,
+ )
+
+ setWindowOnTop(this, isKeyguardShowing)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt
new file mode 100644
index 0000000..19ad44d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2022 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.user.ui.dialog
+
+import android.annotation.UserIdInt
+import android.content.Context
+import android.content.DialogInterface
+import com.android.settingslib.R
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/** Dialog for exiting the guest user. */
+class ExitGuestDialog(
+ context: Context,
+ private val guestUserId: Int,
+ private val isGuestEphemeral: Boolean,
+ private val targetUserId: Int,
+ isKeyguardShowing: Boolean,
+ private val falsingManager: FalsingManager,
+ private val dialogLaunchAnimator: DialogLaunchAnimator,
+ private val onExitGuestUserListener: OnExitGuestUserListener,
+) : SystemUIDialog(context) {
+
+ fun interface OnExitGuestUserListener {
+ fun onExitGuestUser(
+ @UserIdInt guestId: Int,
+ @UserIdInt targetId: Int,
+ forceRemoveGuest: Boolean,
+ )
+ }
+
+ private val onClickListener =
+ object : DialogInterface.OnClickListener {
+ override fun onClick(dialog: DialogInterface, which: Int) {
+ val penalty =
+ if (which == BUTTON_NEGATIVE) {
+ FalsingManager.NO_PENALTY
+ } else {
+ FalsingManager.MODERATE_PENALTY
+ }
+ if (falsingManager.isFalseTap(penalty)) {
+ return
+ }
+
+ if (isGuestEphemeral) {
+ if (which == BUTTON_POSITIVE) {
+ dialogLaunchAnimator.dismissStack(this@ExitGuestDialog)
+ // Ephemeral guest: exit guest, guest is removed by the system
+ // on exit, since its marked ephemeral
+ onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, false)
+ } else if (which == BUTTON_NEGATIVE) {
+ // Cancel clicked, do nothing
+ cancel()
+ }
+ } else {
+ when (which) {
+ BUTTON_POSITIVE -> {
+ dialogLaunchAnimator.dismissStack(this@ExitGuestDialog)
+ // Non-ephemeral guest: exit guest, guest is not removed by the system
+ // on exit, since its marked non-ephemeral
+ onExitGuestUserListener.onExitGuestUser(
+ guestUserId,
+ targetUserId,
+ false
+ )
+ }
+ BUTTON_NEGATIVE -> {
+ dialogLaunchAnimator.dismissStack(this@ExitGuestDialog)
+ // Non-ephemeral guest: remove guest and then exit
+ onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, true)
+ }
+ BUTTON_NEUTRAL -> {
+ // Cancel clicked, do nothing
+ cancel()
+ }
+ }
+ }
+ }
+ }
+
+ init {
+ if (isGuestEphemeral) {
+ setTitle(context.getString(R.string.guest_exit_dialog_title))
+ setMessage(context.getString(R.string.guest_exit_dialog_message))
+ setButton(
+ BUTTON_NEUTRAL,
+ context.getString(android.R.string.cancel),
+ onClickListener,
+ )
+ setButton(
+ BUTTON_POSITIVE,
+ context.getString(R.string.guest_exit_dialog_button),
+ onClickListener,
+ )
+ } else {
+ setTitle(context.getString(R.string.guest_exit_dialog_title_non_ephemeral))
+ setMessage(context.getString(R.string.guest_exit_dialog_message_non_ephemeral))
+ setButton(
+ BUTTON_NEUTRAL,
+ context.getString(android.R.string.cancel),
+ onClickListener,
+ )
+ setButton(
+ BUTTON_NEGATIVE,
+ context.getString(R.string.guest_exit_clear_data_button),
+ onClickListener,
+ )
+ setButton(
+ BUTTON_POSITIVE,
+ context.getString(R.string.guest_exit_save_data_button),
+ onClickListener,
+ )
+ }
+ setWindowOnTop(this, isKeyguardShowing)
+ setCanceledOnTouchOutside(false)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt
new file mode 100644
index 0000000..c1d2f47
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 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.user.ui.dialog
+
+import com.android.systemui.CoreStartable
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface UserDialogModule {
+
+ @Binds
+ @IntoMap
+ @ClassKey(UserSwitcherDialogCoordinator::class)
+ fun bindFeature(impl: UserSwitcherDialogCoordinator): CoreStartable
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
new file mode 100644
index 0000000..6e7b523
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2022 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.user.ui.dialog
+
+import android.app.Dialog
+import android.content.Context
+import com.android.settingslib.users.UserCreatingDialog
+import com.android.systemui.CoreStartable
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.user.domain.interactor.UserInteractor
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.launch
+
+/** Coordinates dialogs for user switcher logic. */
+@SysUISingleton
+class UserSwitcherDialogCoordinator
+@Inject
+constructor(
+ @Application private val context: Context,
+ @Application private val applicationScope: CoroutineScope,
+ private val falsingManager: FalsingManager,
+ private val broadcastSender: BroadcastSender,
+ private val dialogLaunchAnimator: DialogLaunchAnimator,
+ private val interactor: UserInteractor,
+ private val featureFlags: FeatureFlags,
+) : CoreStartable(context) {
+
+ private var currentDialog: Dialog? = null
+
+ override fun start() {
+ if (featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) {
+ return
+ }
+
+ startHandlingDialogShowRequests()
+ startHandlingDialogDismissRequests()
+ }
+
+ private fun startHandlingDialogShowRequests() {
+ applicationScope.launch {
+ interactor.dialogShowRequests.filterNotNull().collect { request ->
+ currentDialog?.let {
+ if (it.isShowing) {
+ it.cancel()
+ }
+ }
+
+ currentDialog =
+ when (request) {
+ is ShowDialogRequestModel.ShowAddUserDialog ->
+ AddUserDialog(
+ context = context,
+ userHandle = request.userHandle,
+ isKeyguardShowing = request.isKeyguardShowing,
+ showEphemeralMessage = request.showEphemeralMessage,
+ falsingManager = falsingManager,
+ broadcastSender = broadcastSender,
+ dialogLaunchAnimator = dialogLaunchAnimator,
+ )
+ is ShowDialogRequestModel.ShowUserCreationDialog ->
+ UserCreatingDialog(
+ context,
+ request.isGuest,
+ )
+ is ShowDialogRequestModel.ShowExitGuestDialog ->
+ ExitGuestDialog(
+ context = context,
+ guestUserId = request.guestUserId,
+ isGuestEphemeral = request.isGuestEphemeral,
+ targetUserId = request.targetUserId,
+ isKeyguardShowing = request.isKeyguardShowing,
+ falsingManager = falsingManager,
+ dialogLaunchAnimator = dialogLaunchAnimator,
+ onExitGuestUserListener = request.onExitGuestUser,
+ )
+ }
+
+ currentDialog?.show()
+ interactor.onDialogShown()
+ }
+ }
+ }
+
+ private fun startHandlingDialogDismissRequests() {
+ applicationScope.launch {
+ interactor.dialogDismissRequests.filterNotNull().collect {
+ currentDialog?.let {
+ if (it.isShowing) {
+ it.cancel()
+ }
+ }
+
+ interactor.onDialogDismissed()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
index 398341d..5b83df7 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt
@@ -21,7 +21,10 @@
import androidx.lifecycle.ViewModelProvider
import com.android.systemui.R
import com.android.systemui.common.ui.drawable.CircularDrawable
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.user.domain.interactor.GuestUserInteractor
import com.android.systemui.user.domain.interactor.UserInteractor
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
@@ -36,9 +39,14 @@
class UserSwitcherViewModel
private constructor(
private val userInteractor: UserInteractor,
+ private val guestUserInteractor: GuestUserInteractor,
private val powerInteractor: PowerInteractor,
+ private val featureFlags: FeatureFlags,
) : ViewModel() {
+ private val isNewImpl: Boolean
+ get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)
+
/** On-device users. */
val users: Flow<List<UserViewModel>> =
userInteractor.users.map { models -> models.map { user -> toViewModel(user) } }
@@ -47,9 +55,6 @@
val maximumUserColumns: Flow<Int> =
users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) }
- /** Whether the button to open the user action menu is visible. */
- val isOpenMenuButtonVisible: Flow<Boolean> = userInteractor.actions.map { it.isNotEmpty() }
-
private val _isMenuVisible = MutableStateFlow(false)
/**
* Whether the user action menu should be shown. Once the action menu is dismissed/closed, the
@@ -58,9 +63,23 @@
val isMenuVisible: Flow<Boolean> = _isMenuVisible
/** The user action menu. */
val menu: Flow<List<UserActionViewModel>> =
- userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } }
+ userInteractor.actions.map { actions ->
+ if (isNewImpl && actions.isNotEmpty()) {
+ // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user
+ // switcher specific action that is not known to the our data source or other
+ // features.
+ actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+ } else {
+ actions
+ }
+ .map { action -> toViewModel(action) }
+ }
+
+ /** Whether the button to open the user action menu is visible. */
+ val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() }
private val hasCancelButtonBeenClicked = MutableStateFlow(false)
+ private val isFinishRequiredDueToExecutedAction = MutableStateFlow(false)
/**
* Whether the observer should finish the experience. Once consumed, [onFinished] must be called
@@ -81,6 +100,7 @@
*/
fun onFinished() {
hasCancelButtonBeenClicked.value = false
+ isFinishRequiredDueToExecutedAction.value = false
}
/** Notifies that the user has clicked the "open menu" button. */
@@ -120,8 +140,10 @@
},
// When the cancel button is clicked, we should finish.
hasCancelButtonBeenClicked,
- ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked ->
- selectedUserChanged || screenTurnedOff || cancelButtonClicked
+ // If an executed action told us to finish, we should finish,
+ isFinishRequiredDueToExecutedAction,
+ ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked, executedActionFinish ->
+ selectedUserChanged || screenTurnedOff || cancelButtonClicked || executedActionFinish
}
}
@@ -164,13 +186,25 @@
} else {
LegacyUserUiHelper.getUserSwitcherActionTextResourceId(
isGuest = model == UserActionModel.ENTER_GUEST_MODE,
- isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated,
- isGuestUserResetting = userInteractor.isGuestUserResetting,
+ isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated,
+ isGuestUserResetting = guestUserInteractor.isGuestUserResetting,
isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER,
isAddUser = model == UserActionModel.ADD_USER,
)
},
- onClicked = { userInteractor.executeAction(action = model) },
+ onClicked = {
+ userInteractor.executeAction(action = model)
+ // We don't finish because we want to show a dialog over the full-screen UI and
+ // that dialog can be dismissed in case the user changes their mind and decides not
+ // to add a user.
+ //
+ // We finish for all other actions because they navigate us away from the
+ // full-screen experience or are destructive (like changing to the guest user).
+ val shouldFinish = model != UserActionModel.ADD_USER
+ if (shouldFinish) {
+ isFinishRequiredDueToExecutedAction.value = true
+ }
+ },
)
}
@@ -186,13 +220,17 @@
@Inject
constructor(
private val userInteractor: UserInteractor,
+ private val guestUserInteractor: GuestUserInteractor,
private val powerInteractor: PowerInteractor,
+ private val featureFlags: FeatureFlags,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return UserSwitcherViewModel(
userInteractor = userInteractor,
+ guestUserInteractor = guestUserInteractor,
powerInteractor = powerInteractor,
+ featureFlags = featureFlags,
)
as T
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt
new file mode 100644
index 0000000..0b8257d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 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.util.settings
+
+import android.annotation.UserIdInt
+import android.database.ContentObserver
+import android.os.UserHandle
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Kotlin extension functions for [SettingsProxy]. */
+object SettingsProxyExt {
+
+ /** Returns a flow of [Unit] that is invoked each time that content is updated. */
+ fun SettingsProxy.observerFlow(
+ vararg names: String,
+ @UserIdInt userId: Int = UserHandle.USER_CURRENT,
+ ): Flow<Unit> {
+ return conflatedCallbackFlow {
+ val observer =
+ object : ContentObserver(null) {
+ override fun onChange(selfChange: Boolean) {
+ trySend(Unit)
+ }
+ }
+
+ names.forEach { name -> registerContentObserverForUser(name, observer, userId) }
+
+ awaitClose { unregisterContentObserver(observer) }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 43f6f1a..c1036e3 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -411,7 +411,7 @@
0 /* flags */);
users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */,
false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */,
- false /* isAddSupervisedUser */));
+ false /* isAddSupervisedUser */, null /* enforcedAdmin */));
}
return users;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index ba1e168..eea2e95 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -116,6 +116,7 @@
val job = underTest.isKeyguardShowing.onEach { latest = it }.launchIn(this)
assertThat(latest).isFalse()
+ assertThat(underTest.isKeyguardShowing()).isFalse()
val captor = argumentCaptor<KeyguardStateController.Callback>()
verify(keyguardStateController).addCallback(captor.capture())
@@ -123,10 +124,12 @@
whenever(keyguardStateController.isShowing).thenReturn(true)
captor.value.onKeyguardShowingChanged()
assertThat(latest).isTrue()
+ assertThat(underTest.isKeyguardShowing()).isTrue()
whenever(keyguardStateController.isShowing).thenReturn(false)
captor.value.onKeyguardShowingChanged()
assertThat(latest).isFalse()
+ assertThat(underTest.isKeyguardShowing()).isFalse()
job.cancel()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
index ff0faf9..098086a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
@@ -35,7 +35,9 @@
import com.android.internal.statusbar.IUndoMediaTransferCallback
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.media.taptotransfer.common.MediaTttLogger
+import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.concurrency.FakeExecutor
@@ -43,10 +45,12 @@
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
+import dagger.Lazy
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
@@ -75,6 +79,14 @@
private lateinit var windowManager: WindowManager
@Mock
private lateinit var commandQueue: CommandQueue
+ @Mock
+ private lateinit var lazyFalsingManager: Lazy<FalsingManager>
+ @Mock
+ private lateinit var falsingManager: FalsingManager
+ @Mock
+ private lateinit var lazyFalsingCollector: Lazy<FalsingCollector>
+ @Mock
+ private lateinit var falsingCollector: FalsingCollector
private lateinit var commandQueueCallback: CommandQueue.Callbacks
private lateinit var fakeAppIconDrawable: Drawable
private lateinit var fakeClock: FakeSystemClock
@@ -101,6 +113,8 @@
senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake)
whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT)
+ whenever(lazyFalsingManager.get()).thenReturn(falsingManager)
+ whenever(lazyFalsingCollector.get()).thenReturn(falsingCollector)
controllerSender = MediaTttChipControllerSender(
commandQueue,
@@ -111,7 +125,9 @@
accessibilityManager,
configurationController,
powerManager,
- senderUiEventLogger
+ senderUiEventLogger,
+ lazyFalsingManager,
+ lazyFalsingCollector
)
val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java)
@@ -417,6 +433,38 @@
}
@Test
+ fun transferToReceiverSucceeded_withUndoRunnable_falseTap_callbackNotRun() {
+ whenever(lazyFalsingManager.get().isFalseTap(anyInt())).thenReturn(true)
+ var undoCallbackCalled = false
+ val undoCallback = object : IUndoMediaTransferCallback.Stub() {
+ override fun onUndoTriggered() {
+ undoCallbackCalled = true
+ }
+ }
+
+ controllerSender.displayView(transferToReceiverSucceeded(undoCallback))
+ getChipView().getUndoButton().performClick()
+
+ assertThat(undoCallbackCalled).isFalse()
+ }
+
+ @Test
+ fun transferToReceiverSucceeded_withUndoRunnable_realTap_callbackRun() {
+ whenever(lazyFalsingManager.get().isFalseTap(anyInt())).thenReturn(false)
+ var undoCallbackCalled = false
+ val undoCallback = object : IUndoMediaTransferCallback.Stub() {
+ override fun onUndoTriggered() {
+ undoCallbackCalled = true
+ }
+ }
+
+ controllerSender.displayView(transferToReceiverSucceeded(undoCallback))
+ getChipView().getUndoButton().performClick()
+
+ assertThat(undoCallbackCalled).isTrue()
+ }
+
+ @Test
fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() {
val undoCallback = object : IUndoMediaTransferCallback.Stub() {
override fun onUndoTriggered() {}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index 5d5918d..d2c2d58 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -14,6 +14,9 @@
package com.android.systemui.qs;
+import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
+import static com.android.systemui.statusbar.StatusBarState.SHADE;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertTrue;
@@ -49,13 +52,13 @@
import com.android.systemui.flags.Flags;
import com.android.systemui.media.MediaHost;
import com.android.systemui.plugins.FalsingManager;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.customize.QSCustomizerController;
import com.android.systemui.qs.dagger.QSFragmentComponent;
import com.android.systemui.qs.external.TileServiceRequestController;
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
@@ -93,7 +96,7 @@
@Mock private QSPanel.QSTileLayout mQsTileLayout;
@Mock private QSPanel.QSTileLayout mQQsTileLayout;
@Mock private QSAnimator mQSAnimator;
- @Mock private StatusBarStateController mStatusBarStateController;
+ @Mock private SysuiStatusBarStateController mStatusBarStateController;
@Mock private QSSquishinessController mSquishinessController;
private View mQsFragmentView;
@@ -158,7 +161,7 @@
public void
transitionToFullShade_onKeyguard_noBouncer_setsAlphaUsingLinearInterpolator() {
QSFragment fragment = resumeAndGetFragment();
- setStatusBarState(StatusBarState.KEYGUARD);
+ setStatusBarState(KEYGUARD);
when(mQSPanelController.isBouncerInTransit()).thenReturn(false);
boolean isTransitioningToFullShade = true;
float transitionProgress = 0.5f;
@@ -174,7 +177,7 @@
public void
transitionToFullShade_onKeyguard_bouncerActive_setsAlphaUsingBouncerInterpolator() {
QSFragment fragment = resumeAndGetFragment();
- setStatusBarState(StatusBarState.KEYGUARD);
+ setStatusBarState(KEYGUARD);
when(mQSPanelController.isBouncerInTransit()).thenReturn(true);
boolean isTransitioningToFullShade = true;
float transitionProgress = 0.5f;
@@ -262,6 +265,27 @@
}
@Test
+ public void setQsExpansion_inSplitShade_whenTransitioningToKeyguard_setsAlphaBasedOnShadeTransitionProgress() {
+ QSFragment fragment = resumeAndGetFragment();
+ enableSplitShade();
+ when(mStatusBarStateController.getState()).thenReturn(SHADE);
+ when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD);
+ boolean isTransitioningToFullShade = false;
+ float transitionProgress = 0;
+ float squishinessFraction = 0f;
+
+ fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress,
+ squishinessFraction);
+
+ // trigger alpha refresh with non-zero expansion and fraction values
+ fragment.setQsExpansion(/* expansion= */ 1, /* panelExpansionFraction= */1,
+ /* proposedTranslation= */ 0, /* squishinessFraction= */ 1);
+
+ // alpha should follow lockscreen to shade progress, not panel expansion fraction
+ assertThat(mQsFragmentView.getAlpha()).isEqualTo(transitionProgress);
+ }
+
+ @Test
public void getQsMinExpansionHeight_notInSplitShade_returnsHeaderHeight() {
QSFragment fragment = resumeAndGetFragment();
disableSplitShade();
@@ -402,6 +426,19 @@
verify(mQSPanelController).setListening(eq(true), anyBoolean());
}
+ @Test
+ public void passCorrectExpansionState_inSplitShade() {
+ QSFragment fragment = resumeAndGetFragment();
+ enableSplitShade();
+ clearInvocations(mQSPanelController);
+
+ fragment.setExpanded(true);
+ verify(mQSPanelController).setExpanded(true);
+
+ fragment.setExpanded(false);
+ verify(mQSPanelController).setExpanded(false);
+ }
+
@Override
protected Fragment instantiate(Context context, String className, Bundle arguments) {
MockitoAnnotations.initMocks(this);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
index 7d56339..4c44dac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java
@@ -33,6 +33,7 @@
import android.graphics.Paint;
import android.os.Build;
import android.os.ParcelFileDescriptor;
+import android.os.Process;
import android.provider.MediaStore;
import android.testing.AndroidTestingRunner;
@@ -97,7 +98,8 @@
Bitmap original = createCheckerBitmap(10, 10, 10);
ListenableFuture<ImageExporter.Result> direct =
- exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME);
+ exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME,
+ Process.myUserHandle());
assertTrue("future should be done", direct.isDone());
assertFalse("future should not be canceled", direct.isCancelled());
ImageExporter.Result result = direct.get();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
index 69b7b88..8c9404e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java
@@ -180,7 +180,7 @@
data.finisher = null;
data.mActionsReadyListener = null;
SaveImageInBackgroundTask task =
- new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
+ new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data,
ActionTransition::new, mSmartActionsProvider);
Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(),
@@ -208,7 +208,7 @@
data.finisher = null;
data.mActionsReadyListener = null;
SaveImageInBackgroundTask task =
- new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
+ new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data,
ActionTransition::new, mSmartActionsProvider);
Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(),
@@ -236,7 +236,7 @@
data.finisher = null;
data.mActionsReadyListener = null;
SaveImageInBackgroundTask task =
- new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data,
+ new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data,
ActionTransition::new, mSmartActionsProvider);
Notification.Action deleteAction = task.createDeleteAction(mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index b40d5ac..0c60d3c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -23,6 +23,9 @@
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
+import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_CLOSED;
+import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPEN;
+import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPENING;
import static com.google.common.truth.Truth.assertThat;
@@ -1249,14 +1252,10 @@
@Test
public void testQsToBeImmediatelyExpandedWhenOpeningPanelInSplitShade() {
enableSplitShade(/* enabled= */ true);
- // set panel state to CLOSED
- mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 0,
- /* expanded= */ false, /* tracking= */ false, /* dragDownPxAmount= */ 0);
+ mPanelExpansionStateManager.updateState(STATE_CLOSED);
assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
- // change panel state to OPENING
- mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 0.5f,
- /* expanded= */ true, /* tracking= */ true, /* dragDownPxAmount= */ 100);
+ mPanelExpansionStateManager.updateState(STATE_OPENING);
assertThat(mNotificationPanelViewController.mQsExpandImmediate).isTrue();
}
@@ -1264,19 +1263,27 @@
@Test
public void testQsNotToBeImmediatelyExpandedWhenGoingFromUnlockedToLocked() {
enableSplitShade(/* enabled= */ true);
- // set panel state to CLOSED
- mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 0,
- /* expanded= */ false, /* tracking= */ false, /* dragDownPxAmount= */ 0);
+ mPanelExpansionStateManager.updateState(STATE_CLOSED);
- // go to lockscreen, which also sets fraction to 1.0f and makes shade "expanded"
mStatusBarStateController.setState(KEYGUARD);
- mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1,
- /* expanded= */ true, /* tracking= */ true, /* dragDownPxAmount= */ 0);
+ // going to lockscreen would trigger STATE_OPENING
+ mPanelExpansionStateManager.updateState(STATE_OPENING);
assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
}
@Test
+ public void testQsImmediateResetsWhenPanelOpensOrCloses() {
+ mNotificationPanelViewController.mQsExpandImmediate = true;
+ mPanelExpansionStateManager.updateState(STATE_OPEN);
+ assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
+
+ mNotificationPanelViewController.mQsExpandImmediate = true;
+ mPanelExpansionStateManager.updateState(STATE_CLOSED);
+ assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse();
+ }
+
+ @Test
public void testQsExpansionChangedToDefaultWhenRotatingFromOrToSplitShade() {
// to make sure shade is in expanded state
mNotificationPanelViewController.startWaitingForOpenPanelGesture();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 2ee3126..2970807 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -337,6 +337,40 @@
}
@Test
+ fun testOnEntryUpdated_toAlert() {
+ // GIVEN that an entry is posted that should not heads up
+ setShouldHeadsUp(mEntry, false)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // WHEN it's updated to heads up
+ setShouldHeadsUp(mEntry)
+ mCollectionListener.onEntryUpdated(mEntry)
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN the notification alerts
+ finishBind(mEntry)
+ verify(mHeadsUpManager).showNotification(mEntry)
+ }
+
+ @Test
+ fun testOnEntryUpdated_toNotAlert() {
+ // GIVEN that an entry is posted that should heads up
+ setShouldHeadsUp(mEntry)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // WHEN it's updated to not heads up
+ setShouldHeadsUp(mEntry, false)
+ mCollectionListener.onEntryUpdated(mEntry)
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN the notification is never bound or shown
+ verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+ verify(mHeadsUpManager, never()).showNotification(any())
+ }
+
+ @Test
fun testOnEntryRemovedRemovesHeadsUpNotification() {
// GIVEN the current HUN is mEntry
addHUN(mEntry)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
index 7e97629..dae0aa2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java
@@ -40,6 +40,10 @@
NotificationPanelLogger.toNotificationProto(visibleNotifications)));
}
+ @Override
+ public void logNotificationDrag(NotificationEntry draggedNotification) {
+ }
+
public static class CallRecord {
public boolean isLockscreen;
public Notifications.NotificationList list;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
index 922e93d..ed2afe7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
@@ -40,6 +40,8 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.shade.ShadeController;
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger;
+import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerFake;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import org.junit.Before;
@@ -63,6 +65,7 @@
private NotificationMenuRowPlugin.MenuItem mMenuItem =
mock(NotificationMenuRowPlugin.MenuItem.class);
private ShadeController mShadeController = mock(ShadeController.class);
+ private NotificationPanelLogger mNotificationPanelLogger = mock(NotificationPanelLogger.class);
@Before
public void setUp() throws Exception {
@@ -82,7 +85,7 @@
when(mMenuRow.getLongpressMenuItem(any(Context.class))).thenReturn(mMenuItem);
mController = new ExpandableNotificationRowDragController(mContext, mHeadsUpManager,
- mShadeController);
+ mShadeController, mNotificationPanelLogger);
}
@Test
@@ -96,6 +99,7 @@
mRow.doDragCallback(0, 0);
verify(controller).startDragAndDrop(mRow);
verify(mHeadsUpManager, times(1)).releaseAllImmediately();
+ verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any());
}
@Test
@@ -107,6 +111,7 @@
verify(controller).startDragAndDrop(mRow);
verify(mShadeController).animateCollapsePanels(eq(0), eq(true),
eq(false), anyFloat());
+ verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any());
}
@Test
@@ -124,6 +129,7 @@
// Verify that we never start the actual drag since there is no content
verify(mRow, never()).startDragAndDrop(any(), any(), any(), anyInt());
+ verify(mNotificationPanelLogger, never()).logNotificationDrag(any());
}
private ExpandableNotificationRowDragController createSpyController() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt
new file mode 100644
index 0000000..773a0d8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 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.telephony.data.repository
+
+import android.telephony.TelephonyCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.telephony.TelephonyListenerManager
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class TelephonyRepositoryImplTest : SysuiTestCase() {
+
+ @Mock private lateinit var manager: TelephonyListenerManager
+
+ private lateinit var underTest: TelephonyRepositoryImpl
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ underTest =
+ TelephonyRepositoryImpl(
+ manager = manager,
+ )
+ }
+
+ @Test
+ fun callState() =
+ runBlocking(IMMEDIATE) {
+ var callState: Int? = null
+ val job = underTest.callState.onEach { callState = it }.launchIn(this)
+ val listenerCaptor = kotlinArgumentCaptor<TelephonyCallback.CallStateListener>()
+ verify(manager).addCallStateListener(listenerCaptor.capture())
+ val listener = listenerCaptor.value
+
+ listener.onCallStateChanged(0)
+ assertThat(callState).isEqualTo(0)
+
+ listener.onCallStateChanged(1)
+ assertThat(callState).isEqualTo(1)
+
+ listener.onCallStateChanged(2)
+ assertThat(callState).isEqualTo(2)
+
+ job.cancel()
+
+ verify(manager).removeCallStateListener(listener)
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
new file mode 100644
index 0000000..4a8e055
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2022 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.user.data.repository
+
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(JUnit4::class)
+class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() {
+
+ @Before
+ fun setUp() {
+ super.setUp(isRefactored = true)
+ }
+
+ @Test
+ fun userSwitcherSettings() = runSelfCancelingTest {
+ setUpGlobalSettings(
+ isSimpleUserSwitcher = true,
+ isAddUsersFromLockscreen = true,
+ isUserSwitcherEnabled = true,
+ )
+ underTest = create(this)
+
+ var value: UserSwitcherSettingsModel? = null
+ underTest.userSwitcherSettings.onEach { value = it }.launchIn(this)
+
+ assertUserSwitcherSettings(
+ model = value,
+ expectedSimpleUserSwitcher = true,
+ expectedAddUsersFromLockscreen = true,
+ expectedUserSwitcherEnabled = true,
+ )
+
+ setUpGlobalSettings(
+ isSimpleUserSwitcher = false,
+ isAddUsersFromLockscreen = true,
+ isUserSwitcherEnabled = true,
+ )
+ assertUserSwitcherSettings(
+ model = value,
+ expectedSimpleUserSwitcher = false,
+ expectedAddUsersFromLockscreen = true,
+ expectedUserSwitcherEnabled = true,
+ )
+ }
+
+ @Test
+ fun refreshUsers() = runSelfCancelingTest {
+ underTest = create(this)
+ val initialExpectedValue =
+ setUpUsers(
+ count = 3,
+ selectedIndex = 0,
+ )
+ var userInfos: List<UserInfo>? = null
+ var selectedUserInfo: UserInfo? = null
+ underTest.userInfos.onEach { userInfos = it }.launchIn(this)
+ underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this)
+
+ underTest.refreshUsers()
+ assertThat(userInfos).isEqualTo(initialExpectedValue)
+ assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0])
+ assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
+
+ val secondExpectedValue =
+ setUpUsers(
+ count = 4,
+ selectedIndex = 1,
+ )
+ underTest.refreshUsers()
+ assertThat(userInfos).isEqualTo(secondExpectedValue)
+ assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1])
+ assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id)
+
+ val selectedNonGuestUserId = selectedUserInfo?.id
+ val thirdExpectedValue =
+ setUpUsers(
+ count = 2,
+ hasGuest = true,
+ selectedIndex = 1,
+ )
+ underTest.refreshUsers()
+ assertThat(userInfos).isEqualTo(thirdExpectedValue)
+ assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1])
+ assertThat(selectedUserInfo?.isGuest).isTrue()
+ assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId)
+ }
+
+ private fun setUpUsers(
+ count: Int,
+ hasGuest: Boolean = false,
+ selectedIndex: Int = 0,
+ ): List<UserInfo> {
+ val userInfos =
+ (0 until count).map { index ->
+ createUserInfo(
+ index,
+ isGuest = hasGuest && index == count - 1,
+ )
+ }
+ whenever(manager.aliveUsers).thenReturn(userInfos)
+ tracker.set(userInfos, selectedIndex)
+ return userInfos
+ }
+
+ private fun createUserInfo(
+ id: Int,
+ isGuest: Boolean,
+ ): UserInfo {
+ val flags = 0
+ return UserInfo(
+ id,
+ "user_$id",
+ /* iconPath= */ "",
+ flags,
+ if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags),
+ )
+ }
+
+ private fun setUpGlobalSettings(
+ isSimpleUserSwitcher: Boolean = false,
+ isAddUsersFromLockscreen: Boolean = false,
+ isUserSwitcherEnabled: Boolean = true,
+ ) {
+ context.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_expandLockScreenUserSwitcher,
+ true,
+ )
+ globalSettings.putIntForUser(
+ UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER,
+ if (isSimpleUserSwitcher) 1 else 0,
+ UserHandle.USER_SYSTEM,
+ )
+ globalSettings.putIntForUser(
+ Settings.Global.ADD_USERS_WHEN_LOCKED,
+ if (isAddUsersFromLockscreen) 1 else 0,
+ UserHandle.USER_SYSTEM,
+ )
+ globalSettings.putIntForUser(
+ Settings.Global.USER_SWITCHER_ENABLED,
+ if (isUserSwitcherEnabled) 1 else 0,
+ UserHandle.USER_SYSTEM,
+ )
+ }
+
+ private fun assertUserSwitcherSettings(
+ model: UserSwitcherSettingsModel?,
+ expectedSimpleUserSwitcher: Boolean,
+ expectedAddUsersFromLockscreen: Boolean,
+ expectedUserSwitcherEnabled: Boolean,
+ ) {
+ checkNotNull(model)
+ assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher)
+ assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen)
+ assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled)
+ }
+
+ /**
+ * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which
+ * is then automatically canceled and cleaned-up.
+ */
+ private fun runSelfCancelingTest(
+ block: suspend CoroutineScope.() -> Unit,
+ ) =
+ runBlocking(Dispatchers.Main.immediate) {
+ val scope = CoroutineScope(coroutineContext + Job())
+ block(scope)
+ scope.cancel()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index 6fec343..dcea83a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -17,201 +17,54 @@
package com.android.systemui.user.data.repository
-import android.content.pm.UserInfo
import android.os.UserManager
-import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.settings.FakeUserTracker
import com.android.systemui.statusbar.policy.UserSwitcherController
-import com.android.systemui.user.data.source.UserRecord
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.user.shared.model.UserModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.capture
-import com.google.common.truth.Truth.assertThat
+import com.android.systemui.util.settings.FakeSettings
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
+import kotlinx.coroutines.test.TestCoroutineScope
import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
-@SmallTest
-@RunWith(JUnit4::class)
-class UserRepositoryImplTest : SysuiTestCase() {
+abstract class UserRepositoryImplTest : SysuiTestCase() {
- @Mock private lateinit var manager: UserManager
- @Mock private lateinit var controller: UserSwitcherController
- @Captor
- private lateinit var userSwitchCallbackCaptor:
- ArgumentCaptor<UserSwitcherController.UserSwitchCallback>
+ @Mock protected lateinit var manager: UserManager
+ @Mock protected lateinit var controller: UserSwitcherController
- private lateinit var underTest: UserRepositoryImpl
+ protected lateinit var underTest: UserRepositoryImpl
- @Before
- fun setUp() {
+ protected lateinit var globalSettings: FakeSettings
+ protected lateinit var tracker: FakeUserTracker
+ protected lateinit var featureFlags: FakeFeatureFlags
+
+ protected fun setUp(isRefactored: Boolean) {
MockitoAnnotations.initMocks(this)
- whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false))
- whenever(controller.isGuestUserAutoCreated).thenReturn(false)
- whenever(controller.isGuestUserResetting).thenReturn(false)
- underTest =
- UserRepositoryImpl(
- appContext = context,
- manager = manager,
- controller = controller,
- )
+ globalSettings = FakeSettings()
+ tracker = FakeUserTracker()
+ featureFlags = FakeFeatureFlags()
+ featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored)
}
- @Test
- fun `users - registers for updates`() =
- runBlocking(IMMEDIATE) {
- val job = underTest.users.onEach {}.launchIn(this)
-
- verify(controller).addUserSwitchCallback(any())
-
- job.cancel()
- }
-
- @Test
- fun `users - unregisters from updates`() =
- runBlocking(IMMEDIATE) {
- val job = underTest.users.onEach {}.launchIn(this)
- verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
-
- job.cancel()
-
- verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
- }
-
- @Test
- fun `users - does not include actions`() =
- runBlocking(IMMEDIATE) {
- whenever(controller.users)
- .thenReturn(
- arrayListOf(
- createUserRecord(0, isSelected = true),
- createActionRecord(UserActionModel.ADD_USER),
- createUserRecord(1),
- createUserRecord(2),
- createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
- createActionRecord(UserActionModel.ENTER_GUEST_MODE),
- )
- )
- var models: List<UserModel>? = null
- val job = underTest.users.onEach { models = it }.launchIn(this)
-
- assertThat(models).hasSize(3)
- assertThat(models?.get(0)?.id).isEqualTo(0)
- assertThat(models?.get(0)?.isSelected).isTrue()
- assertThat(models?.get(1)?.id).isEqualTo(1)
- assertThat(models?.get(1)?.isSelected).isFalse()
- assertThat(models?.get(2)?.id).isEqualTo(2)
- assertThat(models?.get(2)?.isSelected).isFalse()
- job.cancel()
- }
-
- @Test
- fun selectedUser() =
- runBlocking(IMMEDIATE) {
- whenever(controller.users)
- .thenReturn(
- arrayListOf(
- createUserRecord(0, isSelected = true),
- createUserRecord(1),
- createUserRecord(2),
- )
- )
- var id: Int? = null
- val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this)
-
- assertThat(id).isEqualTo(0)
-
- whenever(controller.users)
- .thenReturn(
- arrayListOf(
- createUserRecord(0),
- createUserRecord(1),
- createUserRecord(2, isSelected = true),
- )
- )
- verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
- userSwitchCallbackCaptor.value.onUserSwitched()
- assertThat(id).isEqualTo(2)
-
- job.cancel()
- }
-
- @Test
- fun `actions - unregisters from updates`() =
- runBlocking(IMMEDIATE) {
- val job = underTest.actions.onEach {}.launchIn(this)
- verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
-
- job.cancel()
-
- verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
- }
-
- @Test
- fun `actions - registers for updates`() =
- runBlocking(IMMEDIATE) {
- val job = underTest.actions.onEach {}.launchIn(this)
-
- verify(controller).addUserSwitchCallback(any())
-
- job.cancel()
- }
-
- @Test
- fun `actopms - does not include users`() =
- runBlocking(IMMEDIATE) {
- whenever(controller.users)
- .thenReturn(
- arrayListOf(
- createUserRecord(0, isSelected = true),
- createActionRecord(UserActionModel.ADD_USER),
- createUserRecord(1),
- createUserRecord(2),
- createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
- createActionRecord(UserActionModel.ENTER_GUEST_MODE),
- )
- )
- var models: List<UserActionModel>? = null
- val job = underTest.actions.onEach { models = it }.launchIn(this)
-
- assertThat(models).hasSize(3)
- assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER)
- assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER)
- assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE)
- job.cancel()
- }
-
- private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord {
- return UserRecord(
- info = UserInfo(id, "name$id", 0),
- isCurrent = isSelected,
- )
- }
-
- private fun createActionRecord(action: UserActionModel): UserRecord {
- return UserRecord(
- isAddUser = action == UserActionModel.ADD_USER,
- isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER,
- isGuest = action == UserActionModel.ENTER_GUEST_MODE,
+ protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl {
+ return UserRepositoryImpl(
+ appContext = context,
+ manager = manager,
+ controller = controller,
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ backgroundDispatcher = IMMEDIATE,
+ globalSettings = globalSettings,
+ tracker = tracker,
+ featureFlags = featureFlags,
)
}
companion object {
- private val IMMEDIATE = Dispatchers.Main.immediate
+ @JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
new file mode 100644
index 0000000..d4b41c1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2022 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.user.data.repository
+
+import android.content.pm.UserInfo
+import androidx.test.filters.SmallTest
+import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.capture
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+
+@SmallTest
+@RunWith(JUnit4::class)
+class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() {
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+
+ @Captor
+ private lateinit var userSwitchCallbackCaptor:
+ ArgumentCaptor<UserSwitcherController.UserSwitchCallback>
+
+ @Before
+ fun setUp() {
+ super.setUp(isRefactored = false)
+
+ whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false))
+ whenever(controller.isGuestUserAutoCreated).thenReturn(false)
+ whenever(controller.isGuestUserResetting).thenReturn(false)
+
+ underTest = create()
+ }
+
+ @Test
+ fun `users - registers for updates`() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.users.onEach {}.launchIn(this)
+
+ verify(controller).addUserSwitchCallback(any())
+
+ job.cancel()
+ }
+
+ @Test
+ fun `users - unregisters from updates`() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.users.onEach {}.launchIn(this)
+ verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
+
+ job.cancel()
+
+ verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
+ }
+
+ @Test
+ fun `users - does not include actions`() =
+ runBlocking(IMMEDIATE) {
+ whenever(controller.users)
+ .thenReturn(
+ arrayListOf(
+ createUserRecord(0, isSelected = true),
+ createActionRecord(UserActionModel.ADD_USER),
+ createUserRecord(1),
+ createUserRecord(2),
+ createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
+ createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+ )
+ )
+ var models: List<UserModel>? = null
+ val job = underTest.users.onEach { models = it }.launchIn(this)
+
+ assertThat(models).hasSize(3)
+ assertThat(models?.get(0)?.id).isEqualTo(0)
+ assertThat(models?.get(0)?.isSelected).isTrue()
+ assertThat(models?.get(1)?.id).isEqualTo(1)
+ assertThat(models?.get(1)?.isSelected).isFalse()
+ assertThat(models?.get(2)?.id).isEqualTo(2)
+ assertThat(models?.get(2)?.isSelected).isFalse()
+ job.cancel()
+ }
+
+ @Test
+ fun selectedUser() =
+ runBlocking(IMMEDIATE) {
+ whenever(controller.users)
+ .thenReturn(
+ arrayListOf(
+ createUserRecord(0, isSelected = true),
+ createUserRecord(1),
+ createUserRecord(2),
+ )
+ )
+ var id: Int? = null
+ val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this)
+
+ assertThat(id).isEqualTo(0)
+
+ whenever(controller.users)
+ .thenReturn(
+ arrayListOf(
+ createUserRecord(0),
+ createUserRecord(1),
+ createUserRecord(2, isSelected = true),
+ )
+ )
+ verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
+ userSwitchCallbackCaptor.value.onUserSwitched()
+ assertThat(id).isEqualTo(2)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - unregisters from updates`() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.actions.onEach {}.launchIn(this)
+ verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor))
+
+ job.cancel()
+
+ verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value)
+ }
+
+ @Test
+ fun `actions - registers for updates`() =
+ runBlocking(IMMEDIATE) {
+ val job = underTest.actions.onEach {}.launchIn(this)
+
+ verify(controller).addUserSwitchCallback(any())
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - does not include users`() =
+ runBlocking(IMMEDIATE) {
+ whenever(controller.users)
+ .thenReturn(
+ arrayListOf(
+ createUserRecord(0, isSelected = true),
+ createActionRecord(UserActionModel.ADD_USER),
+ createUserRecord(1),
+ createUserRecord(2),
+ createActionRecord(UserActionModel.ADD_SUPERVISED_USER),
+ createActionRecord(UserActionModel.ENTER_GUEST_MODE),
+ )
+ )
+ var models: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { models = it }.launchIn(this)
+
+ assertThat(models).hasSize(3)
+ assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER)
+ assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER)
+ assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE)
+ job.cancel()
+ }
+
+ private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord {
+ return UserRecord(
+ info = UserInfo(id, "name$id", 0),
+ isCurrent = isSelected,
+ )
+ }
+
+ private fun createActionRecord(action: UserActionModel): UserRecord {
+ return UserRecord(
+ isAddUser = action == UserActionModel.ADD_USER,
+ isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER,
+ isGuest = action == UserActionModel.ENTER_GUEST_MODE,
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
new file mode 100644
index 0000000..120bf79
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2022 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.user.domain.interactor
+
+import android.app.admin.DevicePolicyManager
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestCoroutineScope
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class GuestUserInteractorTest : SysuiTestCase() {
+
+ @Mock private lateinit var manager: UserManager
+ @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+ @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+ @Mock private lateinit var uiEventLogger: UiEventLogger
+ @Mock private lateinit var showDialog: (ShowDialogRequestModel) -> Unit
+ @Mock private lateinit var dismissDialog: () -> Unit
+ @Mock private lateinit var selectUser: (Int) -> Unit
+ @Mock private lateinit var switchUser: (Int) -> Unit
+
+ private lateinit var underTest: GuestUserInteractor
+
+ private lateinit var scope: TestCoroutineScope
+ private lateinit var repository: FakeUserRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(manager.createGuest(any())).thenReturn(GUEST_USER_INFO)
+
+ scope = TestCoroutineScope()
+ repository = FakeUserRepository()
+ repository.setUserInfos(ALL_USERS)
+
+ underTest =
+ GuestUserInteractor(
+ applicationContext = context,
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ backgroundDispatcher = IMMEDIATE,
+ manager = manager,
+ repository = repository,
+ deviceProvisionedController = deviceProvisionedController,
+ devicePolicyManager = devicePolicyManager,
+ refreshUsersScheduler =
+ RefreshUsersScheduler(
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ repository = repository,
+ ),
+ uiEventLogger = uiEventLogger,
+ )
+ }
+
+ @Test
+ fun `onDeviceBootCompleted - allowed to add - create guest`() =
+ runBlocking(IMMEDIATE) {
+ setAllowedToAdd()
+
+ underTest.onDeviceBootCompleted()
+
+ verify(manager).createGuest(any())
+ verify(deviceProvisionedController, never()).addCallback(any())
+ }
+
+ @Test
+ fun `onDeviceBootCompleted - await provisioning - and create guest`() =
+ runBlocking(IMMEDIATE) {
+ setAllowedToAdd(isAllowed = false)
+ underTest.onDeviceBootCompleted()
+ val captor =
+ kotlinArgumentCaptor<DeviceProvisionedController.DeviceProvisionedListener>()
+ verify(deviceProvisionedController).addCallback(captor.capture())
+
+ setAllowedToAdd(isAllowed = true)
+ captor.value.onDeviceProvisionedChanged()
+
+ verify(manager).createGuest(any())
+ verify(deviceProvisionedController).removeCallback(captor.value)
+ }
+
+ @Test
+ fun createAndSwitchTo() =
+ runBlocking(IMMEDIATE) {
+ underTest.createAndSwitchTo(
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ selectUser = selectUser,
+ )
+
+ verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
+ verify(manager).createGuest(any())
+ verify(dismissDialog).invoke()
+ verify(selectUser).invoke(GUEST_USER_INFO.id)
+ }
+
+ @Test
+ fun `createAndSwitchTo - fails to create - does not switch to`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.createGuest(any())).thenReturn(null)
+
+ underTest.createAndSwitchTo(
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ selectUser = selectUser,
+ )
+
+ verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true))
+ verify(manager).createGuest(any())
+ verify(dismissDialog).invoke()
+ verify(selectUser, never()).invoke(anyInt())
+ }
+
+ @Test
+ fun `exit - returns to target user`() =
+ runBlocking(IMMEDIATE) {
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+ val targetUserId = NON_GUEST_USER_INFO.id
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = targetUserId,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager, never()).markGuestForDeletion(anyInt())
+ verify(manager, never()).removeUser(anyInt())
+ verify(switchUser).invoke(targetUserId)
+ }
+
+ @Test
+ fun `exit - returns to last non-guest`() =
+ runBlocking(IMMEDIATE) {
+ val expectedUserId = NON_GUEST_USER_INFO.id
+ whenever(manager.getUserInfo(expectedUserId)).thenReturn(NON_GUEST_USER_INFO)
+ repository.lastSelectedNonGuestUserId = expectedUserId
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = UserHandle.USER_NULL,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager, never()).markGuestForDeletion(anyInt())
+ verify(manager, never()).removeUser(anyInt())
+ verify(switchUser).invoke(expectedUserId)
+ }
+
+ @Test
+ fun `exit - last non-guest was removed - returns to system`() =
+ runBlocking(IMMEDIATE) {
+ val removedUserId = 310
+ repository.lastSelectedNonGuestUserId = removedUserId
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = UserHandle.USER_NULL,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager, never()).markGuestForDeletion(anyInt())
+ verify(manager, never()).removeUser(anyInt())
+ verify(switchUser).invoke(UserHandle.USER_SYSTEM)
+ }
+
+ @Test
+ fun `exit - guest was ephemeral - it is removed`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setUserInfos(listOf(NON_GUEST_USER_INFO, EPHEMERAL_GUEST_USER_INFO))
+ repository.setSelectedUserInfo(EPHEMERAL_GUEST_USER_INFO)
+ val targetUserId = NON_GUEST_USER_INFO.id
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = targetUserId,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager).markGuestForDeletion(EPHEMERAL_GUEST_USER_INFO.id)
+ verify(manager).removeUser(EPHEMERAL_GUEST_USER_INFO.id)
+ verify(switchUser).invoke(targetUserId)
+ }
+
+ @Test
+ fun `exit - force remove guest - it is removed`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+ val targetUserId = NON_GUEST_USER_INFO.id
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = targetUserId,
+ forceRemoveGuestOnExit = true,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager).markGuestForDeletion(GUEST_USER_INFO.id)
+ verify(manager).removeUser(GUEST_USER_INFO.id)
+ verify(switchUser).invoke(targetUserId)
+ }
+
+ @Test
+ fun `exit - selected different from guest user - do nothing`() =
+ runBlocking(IMMEDIATE) {
+ repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+ underTest.exit(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = 123,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verifyDidNotExit()
+ }
+
+ @Test
+ fun `exit - selected is actually not a guest user - do nothing`() =
+ runBlocking(IMMEDIATE) {
+ repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+ underTest.exit(
+ guestUserId = NON_GUEST_USER_INFO.id,
+ targetUserId = 123,
+ forceRemoveGuestOnExit = false,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verifyDidNotExit()
+ }
+
+ @Test
+ fun `remove - returns to target user`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setSelectedUserInfo(GUEST_USER_INFO)
+
+ val targetUserId = NON_GUEST_USER_INFO.id
+ underTest.remove(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = targetUserId,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verify(manager).markGuestForDeletion(GUEST_USER_INFO.id)
+ verify(manager).removeUser(GUEST_USER_INFO.id)
+ verify(switchUser).invoke(targetUserId)
+ }
+
+ @Test
+ fun `remove - selected different from guest user - do nothing`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+ underTest.remove(
+ guestUserId = GUEST_USER_INFO.id,
+ targetUserId = 123,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verifyDidNotRemove()
+ }
+
+ @Test
+ fun `remove - selected is actually not a guest user - do nothing`() =
+ runBlocking(IMMEDIATE) {
+ whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true)
+ repository.setSelectedUserInfo(NON_GUEST_USER_INFO)
+
+ underTest.remove(
+ guestUserId = NON_GUEST_USER_INFO.id,
+ targetUserId = 123,
+ showDialog = showDialog,
+ dismissDialog = dismissDialog,
+ switchUser = switchUser,
+ )
+
+ verifyDidNotRemove()
+ }
+
+ private fun setAllowedToAdd(isAllowed: Boolean = true) {
+ whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(isAllowed)
+ whenever(devicePolicyManager.isDeviceManaged).thenReturn(!isAllowed)
+ }
+
+ private fun verifyDidNotExit() {
+ verifyDidNotRemove()
+ verify(manager, never()).getUserInfo(anyInt())
+ verify(uiEventLogger, never()).log(any())
+ }
+
+ private fun verifyDidNotRemove() {
+ verify(manager, never()).markGuestForDeletion(anyInt())
+ verify(showDialog, never()).invoke(any())
+ verify(dismissDialog, never()).invoke()
+ verify(switchUser, never()).invoke(anyInt())
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ private val NON_GUEST_USER_INFO =
+ UserInfo(
+ /* id= */ 818,
+ /* name= */ "non_guest",
+ /* flags= */ 0,
+ )
+ private val GUEST_USER_INFO =
+ UserInfo(
+ /* id= */ 669,
+ /* name= */ "guest",
+ /* iconPath= */ "",
+ /* flags= */ 0,
+ UserManager.USER_TYPE_FULL_GUEST,
+ )
+ private val EPHEMERAL_GUEST_USER_INFO =
+ UserInfo(
+ /* id= */ 669,
+ /* name= */ "guest",
+ /* iconPath= */ "",
+ /* flags= */ UserInfo.FLAG_EPHEMERAL,
+ UserManager.USER_TYPE_FULL_GUEST,
+ )
+ private val ALL_USERS =
+ listOf(
+ NON_GUEST_USER_INFO,
+ GUEST_USER_INFO,
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
new file mode 100644
index 0000000..593ce1f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2022 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.user.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class RefreshUsersSchedulerTest : SysuiTestCase() {
+
+ private lateinit var underTest: RefreshUsersScheduler
+
+ private lateinit var repository: FakeUserRepository
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ repository = FakeUserRepository()
+ }
+
+ @Test
+ fun `pause - prevents the next refresh from happening`() =
+ runBlocking(IMMEDIATE) {
+ underTest =
+ RefreshUsersScheduler(
+ applicationScope = this,
+ mainDispatcher = IMMEDIATE,
+ repository = repository,
+ )
+ underTest.pause()
+
+ underTest.refreshIfNotPaused()
+ assertThat(repository.refreshUsersCallCount).isEqualTo(0)
+ }
+
+ @Test
+ fun `unpauseAndRefresh - forces the refresh even when paused`() =
+ runBlocking(IMMEDIATE) {
+ underTest =
+ RefreshUsersScheduler(
+ applicationScope = this,
+ mainDispatcher = IMMEDIATE,
+ repository = repository,
+ )
+ underTest.pause()
+
+ underTest.unpauseAndRefresh()
+
+ assertThat(repository.refreshUsersCallCount).isEqualTo(1)
+ }
+
+ @Test
+ fun `refreshIfNotPaused - refreshes when not paused`() =
+ runBlocking(IMMEDIATE) {
+ underTest =
+ RefreshUsersScheduler(
+ applicationScope = this,
+ mainDispatcher = IMMEDIATE,
+ repository = repository,
+ )
+ underTest.refreshIfNotPaused()
+
+ assertThat(repository.refreshUsersCallCount).isEqualTo(1)
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
new file mode 100644
index 0000000..3d5695a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt
@@ -0,0 +1,658 @@
+/*
+ * Copyright (C) 2022 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.user.domain.interactor
+
+import android.content.Intent
+import android.content.pm.UserInfo
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.internal.R.drawable.ic_account_circle
+import com.android.systemui.R
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.domain.model.ShowDialogRequestModel
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.user.shared.model.UserModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(JUnit4::class)
+class UserInteractorRefactoredTest : UserInteractorTest() {
+
+ override fun isRefactored(): Boolean {
+ return true
+ }
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+
+ overrideResource(R.drawable.ic_account_circle, GUEST_ICON)
+ overrideResource(R.dimen.max_avatar_size, 10)
+ overrideResource(
+ com.android.internal.R.string.config_supervisedUserCreationPackage,
+ SUPERVISED_USER_CREATION_APP_PACKAGE,
+ )
+ whenever(manager.getUserIcon(anyInt())).thenReturn(ICON)
+ whenever(manager.canAddMoreUsers(any())).thenReturn(true)
+ }
+
+ @Test
+ fun `users - switcher enabled`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 3, includeGuest = true)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+ var value: List<UserModel>? = null
+ val job = underTest.users.onEach { value = it }.launchIn(this)
+ assertUsers(models = value, count = 3, includeGuest = true)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `users - switches to second user`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+ var value: List<UserModel>? = null
+ val job = underTest.users.onEach { value = it }.launchIn(this)
+ userRepository.setSelectedUserInfo(userInfos[1])
+
+ assertUsers(models = value, count = 2, selectedIndex = 1)
+ job.cancel()
+ }
+
+ @Test
+ fun `users - switcher not enabled`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false))
+
+ var value: List<UserModel>? = null
+ val job = underTest.users.onEach { value = it }.launchIn(this)
+ assertUsers(models = value, count = 1)
+
+ job.cancel()
+ }
+
+ @Test
+ fun selectedUser() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+ var value: UserModel? = null
+ val job = underTest.selectedUser.onEach { value = it }.launchIn(this)
+ assertUser(value, id = 0, isSelected = true)
+
+ userRepository.setSelectedUserInfo(userInfos[1])
+ assertUser(value, id = 1, isSelected = true)
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device unlocked`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ keyguardRepository.setKeyguardShowing(false)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device unlocked user not primary - empty list`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[1])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ keyguardRepository.setKeyguardShowing(false)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value).isEqualTo(emptyList<UserActionModel>())
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device unlocked user is guest - empty list`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = true)
+ assertThat(userInfos[1].isGuest).isTrue()
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[1])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ keyguardRepository.setKeyguardShowing(false)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value).isEqualTo(emptyList<UserActionModel>())
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device locked add from lockscreen set - full list`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(
+ UserSwitcherSettingsModel(
+ isUserSwitcherEnabled = true,
+ isAddUsersFromLockscreen = true,
+ )
+ )
+ keyguardRepository.setKeyguardShowing(false)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - device locked - only guest action is shown`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ keyguardRepository.setKeyguardShowing(true)
+ var value: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { value = it }.launchIn(this)
+
+ assertThat(value).isEqualTo(listOf(UserActionModel.ENTER_GUEST_MODE))
+
+ job.cancel()
+ }
+
+ @Test
+ fun `executeAction - add user - dialog shown`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ keyguardRepository.setKeyguardShowing(false)
+ var dialogRequest: ShowDialogRequestModel? = null
+ val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+ underTest.executeAction(UserActionModel.ADD_USER)
+ assertThat(dialogRequest)
+ .isEqualTo(
+ ShowDialogRequestModel.ShowAddUserDialog(
+ userHandle = userInfos[0].userHandle,
+ isKeyguardShowing = false,
+ showEphemeralMessage = false,
+ )
+ )
+
+ underTest.onDialogShown()
+ assertThat(dialogRequest).isNull()
+
+ job.cancel()
+ }
+
+ @Test
+ fun `executeAction - add supervised user - starts activity`() =
+ runBlocking(IMMEDIATE) {
+ underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
+
+ val intentCaptor = kotlinArgumentCaptor<Intent>()
+ verify(activityStarter).startActivity(intentCaptor.capture(), eq(false))
+ assertThat(intentCaptor.value.action)
+ .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER)
+ assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE)
+ }
+
+ @Test
+ fun `executeAction - navigate to manage users`() =
+ runBlocking(IMMEDIATE) {
+ underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+
+ val intentCaptor = kotlinArgumentCaptor<Intent>()
+ verify(activityStarter).startActivity(intentCaptor.capture(), eq(false))
+ assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS)
+ }
+
+ @Test
+ fun `executeAction - guest mode`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true)
+ whenever(manager.createGuest(any())).thenReturn(guestUserInfo)
+ val dialogRequests = mutableListOf<ShowDialogRequestModel?>()
+ val showDialogsJob =
+ underTest.dialogShowRequests
+ .onEach {
+ dialogRequests.add(it)
+ if (it != null) {
+ underTest.onDialogShown()
+ }
+ }
+ .launchIn(this)
+ val dismissDialogsJob =
+ underTest.dialogDismissRequests
+ .onEach {
+ if (it != null) {
+ underTest.onDialogDismissed()
+ }
+ }
+ .launchIn(this)
+
+ underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
+
+ assertThat(dialogRequests)
+ .contains(
+ ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true),
+ )
+ verify(activityManager).switchUser(guestUserInfo.id)
+
+ showDialogsJob.cancel()
+ dismissDialogsJob.cancel()
+ }
+
+ @Test
+ fun `selectUser - already selected guest re-selected - exit guest dialog`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = true)
+ val guestUserInfo = userInfos[1]
+ assertThat(guestUserInfo.isGuest).isTrue()
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(guestUserInfo)
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ var dialogRequest: ShowDialogRequestModel? = null
+ val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+ underTest.selectUser(newlySelectedUserId = guestUserInfo.id)
+
+ assertThat(dialogRequest)
+ .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
+ job.cancel()
+ }
+
+ @Test
+ fun `selectUser - currently guest non-guest selected - exit guest dialog`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = true)
+ val guestUserInfo = userInfos[1]
+ assertThat(guestUserInfo.isGuest).isTrue()
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(guestUserInfo)
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ var dialogRequest: ShowDialogRequestModel? = null
+ val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+ underTest.selectUser(newlySelectedUserId = userInfos[0].id)
+
+ assertThat(dialogRequest)
+ .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java)
+ job.cancel()
+ }
+
+ @Test
+ fun `selectUser - not currently guest - switches users`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ var dialogRequest: ShowDialogRequestModel? = null
+ val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this)
+
+ underTest.selectUser(newlySelectedUserId = userInfos[1].id)
+
+ assertThat(dialogRequest).isNull()
+ verify(activityManager).switchUser(userInfos[1].id)
+ job.cancel()
+ }
+
+ @Test
+ fun `Telephony call state changes - refreshes users`() =
+ runBlocking(IMMEDIATE) {
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ telephonyRepository.setCallState(1)
+
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+ }
+
+ @Test
+ fun `User switched broadcast`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ val callback1: UserInteractor.UserCallback = mock()
+ val callback2: UserInteractor.UserCallback = mock()
+ underTest.addCallback(callback1)
+ underTest.addCallback(callback2)
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ userRepository.setSelectedUserInfo(userInfos[1])
+ fakeBroadcastDispatcher.registeredReceivers.forEach {
+ it.onReceive(
+ context,
+ Intent(Intent.ACTION_USER_SWITCHED)
+ .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id),
+ )
+ }
+
+ verify(callback1).onUserStateChanged()
+ verify(callback2).onUserStateChanged()
+ assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id)
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+ }
+
+ @Test
+ fun `User info changed broadcast`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach {
+ it.onReceive(
+ context,
+ Intent(Intent.ACTION_USER_INFO_CHANGED),
+ )
+ }
+
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+ }
+
+ @Test
+ fun `System user unlocked broadcast - refresh users`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach {
+ it.onReceive(
+ context,
+ Intent(Intent.ACTION_USER_UNLOCKED)
+ .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM),
+ )
+ }
+
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1)
+ }
+
+ @Test
+ fun `Non-system user unlocked broadcast - do not refresh users`() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 2, includeGuest = false)
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ val refreshUsersCallCount = userRepository.refreshUsersCallCount
+
+ fakeBroadcastDispatcher.registeredReceivers.forEach {
+ it.onReceive(
+ context,
+ Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337),
+ )
+ }
+
+ assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount)
+ }
+
+ @Test
+ fun userRecords() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 3, includeGuest = false)
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ keyguardRepository.setKeyguardShowing(false)
+
+ testCoroutineScope.advanceUntilIdle()
+
+ assertRecords(
+ records = underTest.userRecords.value,
+ userIds = listOf(0, 1, 2),
+ selectedUserIndex = 0,
+ includeGuest = false,
+ expectedActions =
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ ),
+ )
+ }
+
+ @Test
+ fun selectedUserRecord() =
+ runBlocking(IMMEDIATE) {
+ val userInfos = createUserInfos(count = 3, includeGuest = true)
+ userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+ userRepository.setUserInfos(userInfos)
+ userRepository.setSelectedUserInfo(userInfos[0])
+ keyguardRepository.setKeyguardShowing(false)
+
+ assertRecordForUser(
+ record = underTest.selectedUserRecord.value,
+ id = 0,
+ hasPicture = true,
+ isCurrent = true,
+ isSwitchToEnabled = true,
+ )
+ }
+
+ private fun assertUsers(
+ models: List<UserModel>?,
+ count: Int,
+ selectedIndex: Int = 0,
+ includeGuest: Boolean = false,
+ ) {
+ checkNotNull(models)
+ assertThat(models.size).isEqualTo(count)
+ models.forEachIndexed { index, model ->
+ assertUser(
+ model = model,
+ id = index,
+ isSelected = index == selectedIndex,
+ isGuest = includeGuest && index == count - 1
+ )
+ }
+ }
+
+ private fun assertUser(
+ model: UserModel?,
+ id: Int,
+ isSelected: Boolean = false,
+ isGuest: Boolean = false,
+ ) {
+ checkNotNull(model)
+ assertThat(model.id).isEqualTo(id)
+ assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id"))
+ assertThat(model.isSelected).isEqualTo(isSelected)
+ assertThat(model.isSelectable).isTrue()
+ assertThat(model.isGuest).isEqualTo(isGuest)
+ }
+
+ private fun assertRecords(
+ records: List<UserRecord>,
+ userIds: List<Int>,
+ selectedUserIndex: Int = 0,
+ includeGuest: Boolean = false,
+ expectedActions: List<UserActionModel> = emptyList(),
+ ) {
+ assertThat(records.size >= userIds.size).isTrue()
+ userIds.indices.forEach { userIndex ->
+ val record = records[userIndex]
+ assertThat(record.info).isNotNull()
+ val isGuest = includeGuest && userIndex == userIds.size - 1
+ assertRecordForUser(
+ record = record,
+ id = userIds[userIndex],
+ hasPicture = !isGuest,
+ isCurrent = userIndex == selectedUserIndex,
+ isGuest = isGuest,
+ isSwitchToEnabled = true,
+ )
+ }
+
+ assertThat(records.size - userIds.size).isEqualTo(expectedActions.size)
+ (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex ->
+ val record = records[actionIndex]
+ assertThat(record.info).isNull()
+ assertRecordForAction(
+ record = record,
+ type = expectedActions[actionIndex - userIds.size],
+ )
+ }
+ }
+
+ private fun assertRecordForUser(
+ record: UserRecord?,
+ id: Int? = null,
+ hasPicture: Boolean = false,
+ isCurrent: Boolean = false,
+ isGuest: Boolean = false,
+ isSwitchToEnabled: Boolean = false,
+ ) {
+ checkNotNull(record)
+ assertThat(record.info?.id).isEqualTo(id)
+ assertThat(record.picture != null).isEqualTo(hasPicture)
+ assertThat(record.isCurrent).isEqualTo(isCurrent)
+ assertThat(record.isGuest).isEqualTo(isGuest)
+ assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled)
+ }
+
+ private fun assertRecordForAction(
+ record: UserRecord,
+ type: UserActionModel,
+ ) {
+ assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE)
+ assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER)
+ assertThat(record.isAddSupervisedUser)
+ .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER)
+ }
+
+ private fun createUserInfos(
+ count: Int,
+ includeGuest: Boolean,
+ ): List<UserInfo> {
+ return (0 until count).map { index ->
+ val isGuest = includeGuest && index == count - 1
+ createUserInfo(
+ id = index,
+ name =
+ if (isGuest) {
+ "guest"
+ } else {
+ "user_$index"
+ },
+ isPrimary = !isGuest && index == 0,
+ isGuest = isGuest,
+ )
+ }
+ }
+
+ private fun createUserInfo(
+ id: Int,
+ name: String,
+ isPrimary: Boolean = false,
+ isGuest: Boolean = false,
+ ): UserInfo {
+ return UserInfo(
+ id,
+ name,
+ /* iconPath= */ "",
+ /* flags= */ if (isPrimary) {
+ UserInfo.FLAG_PRIMARY
+ } else {
+ 0
+ },
+ if (isGuest) {
+ UserManager.USER_TYPE_FULL_GUEST
+ } else {
+ UserManager.USER_TYPE_FULL_SYSTEM
+ },
+ )
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
+ private val GUEST_ICON: Drawable = mock()
+ private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
index e914e2e..8465f4f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
@@ -17,51 +17,61 @@
package com.android.systemui.user.domain.interactor
-import androidx.test.filters.SmallTest
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
+import android.os.UserManager
+import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
+import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.nullable
-import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import kotlinx.coroutines.test.TestCoroutineScope
import org.mockito.Mock
-import org.mockito.Mockito.anyBoolean
-import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-@SmallTest
-@RunWith(JUnit4::class)
-class UserInteractorTest : SysuiTestCase() {
+abstract class UserInteractorTest : SysuiTestCase() {
- @Mock private lateinit var controller: UserSwitcherController
- @Mock private lateinit var activityStarter: ActivityStarter
+ @Mock protected lateinit var controller: UserSwitcherController
+ @Mock protected lateinit var activityStarter: ActivityStarter
+ @Mock protected lateinit var manager: UserManager
+ @Mock protected lateinit var activityManager: ActivityManager
+ @Mock protected lateinit var deviceProvisionedController: DeviceProvisionedController
+ @Mock protected lateinit var devicePolicyManager: DevicePolicyManager
+ @Mock protected lateinit var uiEventLogger: UiEventLogger
- private lateinit var underTest: UserInteractor
+ protected lateinit var underTest: UserInteractor
- private lateinit var userRepository: FakeUserRepository
- private lateinit var keyguardRepository: FakeKeyguardRepository
+ protected lateinit var testCoroutineScope: TestCoroutineScope
+ protected lateinit var userRepository: FakeUserRepository
+ protected lateinit var keyguardRepository: FakeKeyguardRepository
+ protected lateinit var telephonyRepository: FakeTelephonyRepository
- @Before
- fun setUp() {
+ abstract fun isRefactored(): Boolean
+
+ open fun setUp() {
MockitoAnnotations.initMocks(this)
userRepository = FakeUserRepository()
keyguardRepository = FakeKeyguardRepository()
+ telephonyRepository = FakeTelephonyRepository()
+ testCoroutineScope = TestCoroutineScope()
+ val refreshUsersScheduler =
+ RefreshUsersScheduler(
+ applicationScope = testCoroutineScope,
+ mainDispatcher = IMMEDIATE,
+ repository = userRepository,
+ )
underTest =
UserInteractor(
+ applicationContext = context,
repository = userRepository,
controller = controller,
activityStarter = activityStarter,
@@ -69,142 +79,34 @@
KeyguardInteractor(
repository = keyguardRepository,
),
- )
- }
-
- @Test
- fun `actions - not actionable when locked and locked - no actions`() =
- runBlocking(IMMEDIATE) {
- userRepository.setActions(UserActionModel.values().toList())
- userRepository.setActionableWhenLocked(false)
- keyguardRepository.setKeyguardShowing(true)
-
- var actions: List<UserActionModel>? = null
- val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
- assertThat(actions).isEmpty()
- job.cancel()
- }
-
- @Test
- fun `actions - not actionable when locked and not locked`() =
- runBlocking(IMMEDIATE) {
- userRepository.setActions(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- )
- )
- userRepository.setActionableWhenLocked(false)
- keyguardRepository.setKeyguardShowing(false)
-
- var actions: List<UserActionModel>? = null
- val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
- assertThat(actions)
- .isEqualTo(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ featureFlags =
+ FakeFeatureFlags().apply {
+ set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored())
+ },
+ manager = manager,
+ applicationScope = testCoroutineScope,
+ telephonyInteractor =
+ TelephonyInteractor(
+ repository = telephonyRepository,
+ ),
+ broadcastDispatcher = fakeBroadcastDispatcher,
+ backgroundDispatcher = IMMEDIATE,
+ activityManager = activityManager,
+ refreshUsersScheduler = refreshUsersScheduler,
+ guestUserInteractor =
+ GuestUserInteractor(
+ applicationContext = context,
+ applicationScope = testCoroutineScope,
+ mainDispatcher = IMMEDIATE,
+ backgroundDispatcher = IMMEDIATE,
+ manager = manager,
+ repository = userRepository,
+ deviceProvisionedController = deviceProvisionedController,
+ devicePolicyManager = devicePolicyManager,
+ refreshUsersScheduler = refreshUsersScheduler,
+ uiEventLogger = uiEventLogger,
)
- )
- job.cancel()
- }
-
- @Test
- fun `actions - actionable when locked and not locked`() =
- runBlocking(IMMEDIATE) {
- userRepository.setActions(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- )
)
- userRepository.setActionableWhenLocked(true)
- keyguardRepository.setKeyguardShowing(false)
-
- var actions: List<UserActionModel>? = null
- val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
- assertThat(actions)
- .isEqualTo(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
- )
- )
- job.cancel()
- }
-
- @Test
- fun `actions - actionable when locked and locked`() =
- runBlocking(IMMEDIATE) {
- userRepository.setActions(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- )
- )
- userRepository.setActionableWhenLocked(true)
- keyguardRepository.setKeyguardShowing(true)
-
- var actions: List<UserActionModel>? = null
- val job = underTest.actions.onEach { actions = it }.launchIn(this)
-
- assertThat(actions)
- .isEqualTo(
- listOf(
- UserActionModel.ENTER_GUEST_MODE,
- UserActionModel.ADD_USER,
- UserActionModel.ADD_SUPERVISED_USER,
- UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
- )
- )
- job.cancel()
- }
-
- @Test
- fun selectUser() {
- val userId = 3
-
- underTest.selectUser(userId)
-
- verify(controller).onUserSelected(eq(userId), nullable())
- }
-
- @Test
- fun `executeAction - guest`() {
- underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
-
- verify(controller).createAndSwitchToGuestUser(nullable())
- }
-
- @Test
- fun `executeAction - add user`() {
- underTest.executeAction(UserActionModel.ADD_USER)
-
- verify(controller).showAddUserDialog(nullable())
- }
-
- @Test
- fun `executeAction - add supervised user`() {
- underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
-
- verify(controller).startSupervisedUserActivity()
- }
-
- @Test
- fun `executeAction - manage users`() {
- underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
-
- verify(activityStarter).startActivity(any(), anyBoolean())
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
new file mode 100644
index 0000000..c3a9705
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2022 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.user.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.user.shared.model.UserActionModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.nullable
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(JUnit4::class)
+open class UserInteractorUnrefactoredTest : UserInteractorTest() {
+
+ override fun isRefactored(): Boolean {
+ return false
+ }
+
+ @Before
+ override fun setUp() {
+ super.setUp()
+ }
+
+ @Test
+ fun `actions - not actionable when locked and locked - no actions`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(UserActionModel.values().toList())
+ userRepository.setActionableWhenLocked(false)
+ keyguardRepository.setKeyguardShowing(true)
+
+ var actions: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+ assertThat(actions).isEmpty()
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - not actionable when locked and not locked`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+ userRepository.setActionableWhenLocked(false)
+ keyguardRepository.setKeyguardShowing(false)
+
+ var actions: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+ assertThat(actions)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ )
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - actionable when locked and not locked`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+ userRepository.setActionableWhenLocked(true)
+ keyguardRepository.setKeyguardShowing(false)
+
+ var actions: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+ assertThat(actions)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ )
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun `actions - actionable when locked and locked`() =
+ runBlocking(IMMEDIATE) {
+ userRepository.setActions(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ )
+ )
+ userRepository.setActionableWhenLocked(true)
+ keyguardRepository.setKeyguardShowing(true)
+
+ var actions: List<UserActionModel>? = null
+ val job = underTest.actions.onEach { actions = it }.launchIn(this)
+
+ assertThat(actions)
+ .isEqualTo(
+ listOf(
+ UserActionModel.ENTER_GUEST_MODE,
+ UserActionModel.ADD_USER,
+ UserActionModel.ADD_SUPERVISED_USER,
+ UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+ )
+ )
+ job.cancel()
+ }
+
+ @Test
+ fun selectUser() {
+ val userId = 3
+
+ underTest.selectUser(userId)
+
+ verify(controller).onUserSelected(eq(userId), nullable())
+ }
+
+ @Test
+ fun `executeAction - guest`() {
+ underTest.executeAction(UserActionModel.ENTER_GUEST_MODE)
+
+ verify(controller).createAndSwitchToGuestUser(nullable())
+ }
+
+ @Test
+ fun `executeAction - add user`() {
+ underTest.executeAction(UserActionModel.ADD_USER)
+
+ verify(controller).showAddUserDialog(nullable())
+ }
+
+ @Test
+ fun `executeAction - add supervised user`() {
+ underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER)
+
+ verify(controller).startSupervisedUserActivity()
+ }
+
+ @Test
+ fun `executeAction - manage users`() {
+ underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
+
+ verify(activityStarter).startActivity(any(), anyBoolean())
+ }
+
+ companion object {
+ private val IMMEDIATE = Dispatchers.Main.immediate
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index ef4500d..0344e3f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -17,17 +17,28 @@
package com.android.systemui.user.ui.viewmodel
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
import android.graphics.drawable.Drawable
+import android.os.UserManager
import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Text
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.power.data.repository.FakePowerRepository
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.UserSwitcherController
+import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
+import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.domain.interactor.GuestUserInteractor
+import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
import com.android.systemui.user.domain.interactor.UserInteractor
import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
import com.android.systemui.user.shared.model.UserActionModel
@@ -38,6 +49,7 @@
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.yield
import org.junit.Before
import org.junit.Test
@@ -52,6 +64,11 @@
@Mock private lateinit var controller: UserSwitcherController
@Mock private lateinit var activityStarter: ActivityStarter
+ @Mock private lateinit var activityManager: ActivityManager
+ @Mock private lateinit var manager: UserManager
+ @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+ @Mock private lateinit var devicePolicyManager: DevicePolicyManager
+ @Mock private lateinit var uiEventLogger: UiEventLogger
private lateinit var underTest: UserSwitcherViewModel
@@ -66,22 +83,60 @@
userRepository = FakeUserRepository()
keyguardRepository = FakeKeyguardRepository()
powerRepository = FakePowerRepository()
+ val featureFlags = FakeFeatureFlags()
+ featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, true)
+ val scope = TestCoroutineScope()
+ val refreshUsersScheduler =
+ RefreshUsersScheduler(
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ repository = userRepository,
+ )
+ val guestUserInteractor =
+ GuestUserInteractor(
+ applicationContext = context,
+ applicationScope = scope,
+ mainDispatcher = IMMEDIATE,
+ backgroundDispatcher = IMMEDIATE,
+ manager = manager,
+ repository = userRepository,
+ deviceProvisionedController = deviceProvisionedController,
+ devicePolicyManager = devicePolicyManager,
+ refreshUsersScheduler = refreshUsersScheduler,
+ uiEventLogger = uiEventLogger,
+ )
+
underTest =
UserSwitcherViewModel.Factory(
userInteractor =
UserInteractor(
+ applicationContext = context,
repository = userRepository,
controller = controller,
activityStarter = activityStarter,
keyguardInteractor =
KeyguardInteractor(
repository = keyguardRepository,
- )
+ ),
+ featureFlags = featureFlags,
+ manager = manager,
+ applicationScope = scope,
+ telephonyInteractor =
+ TelephonyInteractor(
+ repository = FakeTelephonyRepository(),
+ ),
+ broadcastDispatcher = fakeBroadcastDispatcher,
+ backgroundDispatcher = IMMEDIATE,
+ activityManager = activityManager,
+ refreshUsersScheduler = refreshUsersScheduler,
+ guestUserInteractor = guestUserInteractor,
),
powerInteractor =
PowerInteractor(
repository = powerRepository,
),
+ featureFlags = featureFlags,
+ guestUserInteractor = guestUserInteractor,
)
.create(UserSwitcherViewModel::class.java)
}
@@ -97,6 +152,7 @@
image = USER_IMAGE,
isSelected = true,
isSelectable = true,
+ isGuest = false,
),
UserModel(
id = 1,
@@ -104,6 +160,7 @@
image = USER_IMAGE,
isSelected = false,
isSelectable = true,
+ isGuest = false,
),
UserModel(
id = 2,
@@ -111,6 +168,7 @@
image = USER_IMAGE,
isSelected = false,
isSelectable = false,
+ isGuest = false,
),
)
)
@@ -260,7 +318,7 @@
job.cancel()
}
- private fun setUsers(count: Int) {
+ private suspend fun setUsers(count: Int) {
userRepository.setUsers(
(0 until count).map { index ->
UserModel(
@@ -269,6 +327,7 @@
image = USER_IMAGE,
isSelected = index == 0,
isSelectable = true,
+ isGuest = false,
)
}
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
index 53dcc8d..bb646f0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
@@ -37,10 +37,18 @@
dumpManager: DumpManager,
logger: BroadcastDispatcherLogger,
userTracker: UserTracker
-) : BroadcastDispatcher(
- context, looper, executor, dumpManager, logger, userTracker, PendingRemovalStore(logger)) {
+) :
+ BroadcastDispatcher(
+ context,
+ looper,
+ executor,
+ dumpManager,
+ logger,
+ userTracker,
+ PendingRemovalStore(logger)
+ ) {
- private val registeredReceivers = ArraySet<BroadcastReceiver>()
+ val registeredReceivers = ArraySet<BroadcastReceiver>()
override fun registerReceiverWithHandler(
receiver: BroadcastReceiver,
@@ -78,4 +86,4 @@
}
registeredReceivers.clear()
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 42b434a..725b1f4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -44,6 +44,10 @@
private val _dozeAmount = MutableStateFlow(0f)
override val dozeAmount: Flow<Float> = _dozeAmount
+ override fun isKeyguardShowing(): Boolean {
+ return _isKeyguardShowing.value
+ }
+
override fun setAnimateDozingTransitions(animate: Boolean) {
_animateBottomAreaDozingTransitions.tryEmit(animate)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
index b2b1764..9726bf8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
@@ -26,20 +26,24 @@
/** A fake [UserTracker] to be used in tests. */
class FakeUserTracker(
- userId: Int = 0,
- userHandle: UserHandle = UserHandle.of(userId),
- userInfo: UserInfo = mock(),
- userProfiles: List<UserInfo> = emptyList(),
+ private var _userId: Int = 0,
+ private var _userHandle: UserHandle = UserHandle.of(_userId),
+ private var _userInfo: UserInfo = mock(),
+ private var _userProfiles: List<UserInfo> = emptyList(),
userContentResolver: ContentResolver = MockContentResolver(),
userContext: Context = mock(),
private val onCreateCurrentUserContext: (Context) -> Context = { mock() },
) : UserTracker {
val callbacks = mutableListOf<UserTracker.Callback>()
- override val userId: Int = userId
- override val userHandle: UserHandle = userHandle
- override val userInfo: UserInfo = userInfo
- override val userProfiles: List<UserInfo> = userProfiles
+ override val userId: Int
+ get() = _userId
+ override val userHandle: UserHandle
+ get() = _userHandle
+ override val userInfo: UserInfo
+ get() = _userInfo
+ override val userProfiles: List<UserInfo>
+ get() = _userProfiles
override val userContentResolver: ContentResolver = userContentResolver
override val userContext: Context = userContext
@@ -55,4 +59,13 @@
override fun createCurrentUserContext(context: Context): Context {
return onCreateCurrentUserContext(context)
}
+
+ fun set(userInfos: List<UserInfo>, selectedUserIndex: Int) {
+ _userProfiles = userInfos
+ _userInfo = userInfos[selectedUserIndex]
+ _userId = _userInfo.id
+ _userHandle = UserHandle.of(_userId)
+
+ callbacks.forEach { it.onUserChanged(_userId, userContext) }
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt
new file mode 100644
index 0000000..59f24ef
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2022 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.telephony.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeTelephonyRepository : TelephonyRepository {
+
+ private val _callState = MutableStateFlow(0)
+ override val callState: Flow<Int> = _callState.asStateFlow()
+
+ fun setCallState(value: Int) {
+ _callState.value = value
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 20f1e36..4df8aa4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -17,12 +17,18 @@
package com.android.systemui.user.data.repository
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import com.android.systemui.user.data.model.UserSwitcherSettingsModel
import com.android.systemui.user.shared.model.UserActionModel
import com.android.systemui.user.shared.model.UserModel
+import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.yield
class FakeUserRepository : UserRepository {
@@ -34,21 +40,71 @@
private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList())
override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow()
+ private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel())
+ override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
+ _userSwitcherSettings.asStateFlow()
+
+ private val _userInfos = MutableStateFlow<List<UserInfo>>(emptyList())
+ override val userInfos: Flow<List<UserInfo>> = _userInfos.asStateFlow()
+
+ private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null)
+ override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull()
+
+ override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM
+
private val _isActionableWhenLocked = MutableStateFlow(false)
override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow()
private var _isGuestUserAutoCreated: Boolean = false
override val isGuestUserAutoCreated: Boolean
get() = _isGuestUserAutoCreated
- private var _isGuestUserResetting: Boolean = false
- override val isGuestUserResetting: Boolean
- get() = _isGuestUserResetting
+
+ override var isGuestUserResetting: Boolean = false
+
+ override val isGuestUserCreationScheduled = AtomicBoolean()
+
+ override var secondaryUserId: Int = UserHandle.USER_NULL
+
+ override var isRefreshUsersPaused: Boolean = false
+
+ var refreshUsersCallCount: Int = 0
+ private set
+
+ override fun refreshUsers() {
+ refreshUsersCallCount++
+ }
+
+ override fun getSelectedUserInfo(): UserInfo {
+ return checkNotNull(_selectedUserInfo.value)
+ }
+
+ override fun isSimpleUserSwitcher(): Boolean {
+ return _userSwitcherSettings.value.isSimpleUserSwitcher
+ }
+
+ fun setUserInfos(infos: List<UserInfo>) {
+ _userInfos.value = infos
+ }
+
+ suspend fun setSelectedUserInfo(userInfo: UserInfo) {
+ check(_userInfos.value.contains(userInfo)) {
+ "Cannot select the following user, it is not in the list of user infos: $userInfo!"
+ }
+
+ _selectedUserInfo.value = userInfo
+ yield()
+ }
+
+ suspend fun setSettings(settings: UserSwitcherSettingsModel) {
+ _userSwitcherSettings.value = settings
+ yield()
+ }
fun setUsers(models: List<UserModel>) {
_users.value = models
}
- fun setSelectedUser(userId: Int) {
+ suspend fun setSelectedUser(userId: Int) {
check(_users.value.find { it.id == userId } != null) {
"Cannot select a user with ID $userId - no user with that ID found!"
}
@@ -62,6 +118,7 @@
}
}
)
+ yield()
}
fun setActions(models: List<UserActionModel>) {
@@ -75,8 +132,4 @@
fun setGuestUserAutoCreated(value: Boolean) {
_isGuestUserAutoCreated = value
}
-
- fun setGuestUserResetting(value: Boolean) {
- _isGuestUserResetting = value
- }
}
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index abc4937..a94e4b9 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -1152,6 +1152,9 @@
}
private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) {
+ if (packageInfo == null) {
+ return;
+ }
if (containsEither(packageInfo.requestedPermissions,
android.Manifest.permission.RUN_IN_BACKGROUND,
android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) {
diff --git a/services/companion/java/com/android/server/companion/PackageUtils.java b/services/companion/java/com/android/server/companion/PackageUtils.java
index f523773..451a700 100644
--- a/services/companion/java/com/android/server/companion/PackageUtils.java
+++ b/services/companion/java/com/android/server/companion/PackageUtils.java
@@ -54,12 +54,19 @@
private static final String PROPERTY_PRIMARY_TAG =
"android.companion.PROPERTY_PRIMARY_COMPANION_DEVICE_SERVICE";
- static @Nullable PackageInfo getPackageInfo(@NonNull Context context,
+ @Nullable
+ static PackageInfo getPackageInfo(@NonNull Context context,
@UserIdInt int userId, @NonNull String packageName) {
final PackageManager pm = context.getPackageManager();
final PackageInfoFlags flags = PackageInfoFlags.of(GET_PERMISSIONS | GET_CONFIGURATIONS);
- return Binder.withCleanCallingIdentity(() ->
- pm.getPackageInfoAsUser(packageName, flags , userId));
+ return Binder.withCleanCallingIdentity(() -> {
+ try {
+ return pm.getPackageInfoAsUser(packageName, flags, userId);
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.e(TAG, "Package [" + packageName + "] is not found.");
+ return null;
+ }
+ });
}
static void enforceUsesCompanionDeviceFeature(@NonNull Context context,
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 9840e0f..9669c06 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -4219,7 +4219,8 @@
final String procName = r.processName;
HostingRecord hostingRecord = new HostingRecord(
HostingRecord.HOSTING_TYPE_SERVICE, r.instanceName,
- r.definingPackageName, r.definingUid, r.serviceInfo.processName);
+ r.definingPackageName, r.definingUid, r.serviceInfo.processName,
+ getHostingRecordTriggerType(r));
ProcessRecord app;
if (!isolated) {
@@ -4323,6 +4324,14 @@
return null;
}
+ private String getHostingRecordTriggerType(ServiceRecord r) {
+ if (Manifest.permission.BIND_JOB_SERVICE.equals(r.permission)
+ && r.mRecentCallingUid == SYSTEM_UID) {
+ return HostingRecord.TRIGGER_TYPE_JOB;
+ }
+ return HostingRecord.TRIGGER_TYPE_UNKNOWN;
+ }
+
private final void requestServiceBindingsLocked(ServiceRecord r, boolean execInFg)
throws TransactionTooLargeException {
for (int i=r.bindings.size()-1; i>=0; i--) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index d4b760f..df5113b 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -5008,7 +5008,8 @@
hostingRecord.getType(),
hostingRecord.getName(),
shortAction,
- HostingRecord.getHostingTypeIdStatsd(hostingRecord.getType()));
+ HostingRecord.getHostingTypeIdStatsd(hostingRecord.getType()),
+ HostingRecord.getTriggerTypeForStatsd(hostingRecord.getTriggerType()));
return true;
}
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 606a09c..207c10c 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -36,6 +36,7 @@
import android.net.INetworkManagementEventObserver;
import android.net.Network;
import android.net.NetworkCapabilities;
+import android.os.BatteryConsumer;
import android.os.BatteryManagerInternal;
import android.os.BatteryStats;
import android.os.BatteryStatsInternal;
@@ -2282,6 +2283,10 @@
pw.println(" --settings: dump the settings key/values related to batterystats");
pw.println(" --cpu: dump cpu stats for debugging purpose");
pw.println(" --power-profile: dump the power profile constants");
+ pw.println(" --usage: write battery usage stats. Optional arguments:");
+ pw.println(" --proto: output as a binary protobuffer");
+ pw.println(" --model power-profile: use the power profile model"
+ + " even if measured energy is available");
pw.println(" <package.name>: optional name of package to filter output by.");
pw.println(" -h: print this help text.");
pw.println("Battery stats (batterystats) commands:");
@@ -2325,6 +2330,31 @@
}
}
+ private void dumpUsageStatsToProto(FileDescriptor fd, PrintWriter pw, int model,
+ boolean proto) {
+ awaitCompletion();
+ syncStats("dump", BatteryExternalStatsWorker.UPDATE_ALL);
+
+ BatteryUsageStatsQuery.Builder builder = new BatteryUsageStatsQuery.Builder()
+ .setMaxStatsAgeMs(0)
+ .includeProcessStateData()
+ .includePowerModels();
+ if (model == BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
+ builder.powerProfileModeledOnly();
+ }
+ BatteryUsageStatsQuery query = builder.build();
+ synchronized (mStats) {
+ mStats.prepareForDumpLocked();
+ BatteryUsageStats batteryUsageStats =
+ mBatteryUsageStatsProvider.getBatteryUsageStats(query);
+ if (proto) {
+ batteryUsageStats.dumpToProto(fd);
+ } else {
+ batteryUsageStats.dump(pw, "");
+ }
+ }
+ }
+
private int doEnableOrDisable(PrintWriter pw, int i, String[] args, boolean enable) {
i++;
if (i >= args.length) {
@@ -2478,6 +2508,35 @@
} else if ("--power-profile".equals(arg)) {
dumpPowerProfile(pw);
return;
+ } else if ("--usage".equals(arg)) {
+ int model = BatteryConsumer.POWER_MODEL_UNDEFINED;
+ boolean proto = false;
+ for (int j = i + 1; j < args.length; j++) {
+ switch (args[j]) {
+ case "--proto":
+ proto = true;
+ break;
+ case "--model": {
+ if (j + 1 < args.length) {
+ j++;
+ if ("power-profile".equals(args[j])) {
+ model = BatteryConsumer.POWER_MODEL_POWER_PROFILE;
+ } else {
+ pw.println("Unknown power model: " + args[j]);
+ dumpHelp(pw);
+ return;
+ }
+ } else {
+ pw.println("--model without a value");
+ dumpHelp(pw);
+ return;
+ }
+ break;
+ }
+ }
+ }
+ dumpUsageStatsToProto(fd, pw, model, proto);
+ return;
} else if ("-a".equals(arg)) {
flags |= BatteryStats.DUMP_VERBOSE;
} else if (arg.length() > 0 && arg.charAt(0) == '-'){
diff --git a/services/core/java/com/android/server/am/BroadcastQueue.java b/services/core/java/com/android/server/am/BroadcastQueue.java
index 5856949..f366cec 100644
--- a/services/core/java/com/android/server/am/BroadcastQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastQueue.java
@@ -1921,7 +1921,7 @@
info.activityInfo.applicationInfo, true,
r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND,
new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST, r.curComponent,
- r.intent.getAction()),
+ r.intent.getAction(), getHostingRecordTriggerType(r)),
isActivityCapable ? ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE : ZYGOTE_POLICY_FLAG_EMPTY,
(r.intent.getFlags() & Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0, false);
if (r.curApp == null) {
@@ -1944,6 +1944,16 @@
mPendingBroadcastRecvIndex = recIdx;
}
+ private String getHostingRecordTriggerType(BroadcastRecord r) {
+ if (r.alarm) {
+ return HostingRecord.TRIGGER_TYPE_ALARM;
+ } else if (r.pushMessage) {
+ return HostingRecord.TRIGGER_TYPE_PUSH_MESSAGE;
+ } else if (r.pushMessageOverQuota) {
+ return HostingRecord.TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA;
+ }
+ return HostingRecord.TRIGGER_TYPE_UNKNOWN;
+ }
@Nullable
private String getTargetPackage(BroadcastRecord r) {
diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java
index ce4528b..baaae1d 100644
--- a/services/core/java/com/android/server/am/BroadcastRecord.java
+++ b/services/core/java/com/android/server/am/BroadcastRecord.java
@@ -71,6 +71,8 @@
final boolean ordered; // serialize the send to receivers?
final boolean sticky; // originated from existing sticky data?
final boolean alarm; // originated from an alarm triggering?
+ final boolean pushMessage; // originated from a push message?
+ final boolean pushMessageOverQuota; // originated from a push message which was over quota?
final boolean initialSticky; // initial broadcast from register to sticky?
final int userId; // user id this broadcast was for
final String resolvedType; // the resolved data type
@@ -309,6 +311,8 @@
mBackgroundActivityStartsToken = backgroundActivityStartsToken;
this.timeoutExempt = timeoutExempt;
alarm = options != null && options.isAlarmBroadcast();
+ pushMessage = options != null && options.isPushMessagingBroadcast();
+ pushMessageOverQuota = options != null && options.isPushMessagingOverQuotaBroadcast();
}
/**
@@ -362,6 +366,8 @@
mBackgroundActivityStartsToken = from.mBackgroundActivityStartsToken;
timeoutExempt = from.timeoutExempt;
alarm = from.alarm;
+ pushMessage = from.pushMessage;
+ pushMessageOverQuota = from.pushMessageOverQuota;
}
/**
diff --git a/services/core/java/com/android/server/am/HostingRecord.java b/services/core/java/com/android/server/am/HostingRecord.java
index f88a8ce..30811a1 100644
--- a/services/core/java/com/android/server/am/HostingRecord.java
+++ b/services/core/java/com/android/server/am/HostingRecord.java
@@ -16,10 +16,30 @@
package com.android.server.am;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_ACTIVITY;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_ADDED_APPLICATION;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_BACKUP;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_BROADCAST;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_CONTENT_PROVIDER;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_EMPTY;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_LINK_FAIL;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_NEXT_ACTIVITY;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_NEXT_TOP_ACTIVITY;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_ON_HOLD;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_RESTART;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_SERVICE;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_SYSTEM;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_TOP_ACTIVITY;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_ALARM;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_JOB;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_PUSH_MESSAGE;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_UNKNOWN;
+import static com.android.internal.util.FrameworkStatsLog.PROCESS_START_TIME__TYPE__UNKNOWN;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
-import android.os.ProcessStartTime;
/**
* This class describes various information required to start a process.
@@ -32,6 +52,9 @@
*
* The {@code mHostingZygote} field describes from which Zygote the new process should be spawned.
*
+ * The {@code mTriggerType} field describes the trigger that started this processs. This could be
+ * an alarm or a push-message for a broadcast, for example. This is purely for logging and stats.
+ *
* {@code mDefiningPackageName} contains the packageName of the package that defines the
* component we want to start; this can be different from the packageName and uid in the
* ApplicationInfo that we're creating the process with, in case the service is a
@@ -71,7 +94,13 @@
public static final String HOSTING_TYPE_TOP_ACTIVITY = "top-activity";
public static final String HOSTING_TYPE_EMPTY = "";
- private @NonNull final String mHostingType;
+ public static final String TRIGGER_TYPE_UNKNOWN = "unknown";
+ public static final String TRIGGER_TYPE_ALARM = "alarm";
+ public static final String TRIGGER_TYPE_PUSH_MESSAGE = "push_message";
+ public static final String TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA = "push_message_over_quota";
+ public static final String TRIGGER_TYPE_JOB = "job";
+
+ @NonNull private final String mHostingType;
private final String mHostingName;
private final int mHostingZygote;
private final String mDefiningPackageName;
@@ -79,11 +108,12 @@
private final boolean mIsTopApp;
private final String mDefiningProcessName;
@Nullable private final String mAction;
+ @NonNull private final String mTriggerType;
public HostingRecord(@NonNull String hostingType) {
this(hostingType, null /* hostingName */, REGULAR_ZYGOTE, null /* definingPackageName */,
-1 /* mDefiningUid */, false /* isTopApp */, null /* definingProcessName */,
- null /* action */);
+ null /* action */, TRIGGER_TYPE_UNKNOWN);
}
public HostingRecord(@NonNull String hostingType, ComponentName hostingName) {
@@ -91,22 +121,24 @@
}
public HostingRecord(@NonNull String hostingType, ComponentName hostingName,
- @Nullable String action) {
+ @Nullable String action, @Nullable String triggerType) {
this(hostingType, hostingName.toShortString(), REGULAR_ZYGOTE,
null /* definingPackageName */, -1 /* mDefiningUid */, false /* isTopApp */,
- null /* definingProcessName */, action);
+ null /* definingProcessName */, action, triggerType);
}
public HostingRecord(@NonNull String hostingType, ComponentName hostingName,
- String definingPackageName, int definingUid, String definingProcessName) {
+ String definingPackageName, int definingUid, String definingProcessName,
+ String triggerType) {
this(hostingType, hostingName.toShortString(), REGULAR_ZYGOTE, definingPackageName,
- definingUid, false /* isTopApp */, definingProcessName, null /* action */);
+ definingUid, false /* isTopApp */, definingProcessName, null /* action */,
+ triggerType);
}
public HostingRecord(@NonNull String hostingType, ComponentName hostingName, boolean isTopApp) {
this(hostingType, hostingName.toShortString(), REGULAR_ZYGOTE,
null /* definingPackageName */, -1 /* mDefiningUid */, isTopApp /* isTopApp */,
- null /* definingProcessName */, null /* action */);
+ null /* definingProcessName */, null /* action */, TRIGGER_TYPE_UNKNOWN);
}
public HostingRecord(@NonNull String hostingType, String hostingName) {
@@ -121,12 +153,12 @@
private HostingRecord(@NonNull String hostingType, String hostingName, int hostingZygote) {
this(hostingType, hostingName, hostingZygote, null /* definingPackageName */,
-1 /* mDefiningUid */, false /* isTopApp */, null /* definingProcessName */,
- null /* action */);
+ null /* action */, TRIGGER_TYPE_UNKNOWN);
}
private HostingRecord(@NonNull String hostingType, String hostingName, int hostingZygote,
String definingPackageName, int definingUid, boolean isTopApp,
- String definingProcessName, @Nullable String action) {
+ String definingProcessName, @Nullable String action, String triggerType) {
mHostingType = hostingType;
mHostingName = hostingName;
mHostingZygote = hostingZygote;
@@ -135,6 +167,7 @@
mIsTopApp = isTopApp;
mDefiningProcessName = definingProcessName;
mAction = action;
+ mTriggerType = triggerType;
}
public @NonNull String getType() {
@@ -188,6 +221,11 @@
return mAction;
}
+ /** Returns the type of trigger that led to this process start. */
+ public @NonNull String getTriggerType() {
+ return mTriggerType;
+ }
+
/**
* Creates a HostingRecord for a process that must spawn from the webview zygote
* @param hostingName name of the component to be hosted in this process
@@ -197,7 +235,7 @@
String definingPackageName, int definingUid, String definingProcessName) {
return new HostingRecord(HostingRecord.HOSTING_TYPE_EMPTY, hostingName.toShortString(),
WEBVIEW_ZYGOTE, definingPackageName, definingUid, false /* isTopApp */,
- definingProcessName, null /* action */);
+ definingProcessName, null /* action */, TRIGGER_TYPE_UNKNOWN);
}
/**
@@ -211,7 +249,7 @@
int definingUid, String definingProcessName) {
return new HostingRecord(HostingRecord.HOSTING_TYPE_EMPTY, hostingName.toShortString(),
APP_ZYGOTE, definingPackageName, definingUid, false /* isTopApp */,
- definingProcessName, null /* action */);
+ definingProcessName, null /* action */, TRIGGER_TYPE_UNKNOWN);
}
/**
@@ -236,35 +274,55 @@
public static int getHostingTypeIdStatsd(@NonNull String hostingType) {
switch(hostingType) {
case HOSTING_TYPE_ACTIVITY:
- return ProcessStartTime.HOSTING_TYPE_ACTIVITY;
+ return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_ACTIVITY;
case HOSTING_TYPE_ADDED_APPLICATION:
- return ProcessStartTime.HOSTING_TYPE_ADDED_APPLICATION;
+ return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_ADDED_APPLICATION;
case HOSTING_TYPE_BACKUP:
- return ProcessStartTime.HOSTING_TYPE_BACKUP;
+ return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_BACKUP;
case HOSTING_TYPE_BROADCAST:
- return ProcessStartTime.HOSTING_TYPE_BROADCAST;
+ return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_BROADCAST;
case HOSTING_TYPE_CONTENT_PROVIDER:
- return ProcessStartTime.HOSTING_TYPE_CONTENT_PROVIDER;
+ return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_CONTENT_PROVIDER;
case HOSTING_TYPE_LINK_FAIL:
- return ProcessStartTime.HOSTING_TYPE_LINK_FAIL;
+ return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_LINK_FAIL;
case HOSTING_TYPE_ON_HOLD:
- return ProcessStartTime.HOSTING_TYPE_ON_HOLD;
+ return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_ON_HOLD;
case HOSTING_TYPE_NEXT_ACTIVITY:
- return ProcessStartTime.HOSTING_TYPE_NEXT_ACTIVITY;
+ return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_NEXT_ACTIVITY;
case HOSTING_TYPE_NEXT_TOP_ACTIVITY:
- return ProcessStartTime.HOSTING_TYPE_NEXT_TOP_ACTIVITY;
+ return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_NEXT_TOP_ACTIVITY;
case HOSTING_TYPE_RESTART:
- return ProcessStartTime.HOSTING_TYPE_RESTART;
+ return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_RESTART;
case HOSTING_TYPE_SERVICE:
- return ProcessStartTime.HOSTING_TYPE_SERVICE;
+ return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_SERVICE;
case HOSTING_TYPE_SYSTEM:
- return ProcessStartTime.HOSTING_TYPE_SYSTEM;
+ return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_SYSTEM;
case HOSTING_TYPE_TOP_ACTIVITY:
- return ProcessStartTime.HOSTING_TYPE_TOP_ACTIVITY;
+ return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_TOP_ACTIVITY;
case HOSTING_TYPE_EMPTY:
- return ProcessStartTime.HOSTING_TYPE_EMPTY;
+ return PROCESS_START_TIME__HOSTING_TYPE_ID__HOSTING_TYPE_EMPTY;
default:
- return ProcessStartTime.HOSTING_TYPE_UNKNOWN;
+ return PROCESS_START_TIME__TYPE__UNKNOWN;
+ }
+ }
+
+ /**
+ * Map the string triggerType to enum TriggerType defined in ProcessStartTime proto.
+ * @param triggerType
+ * @return enum TriggerType defined in ProcessStartTime proto
+ */
+ public static int getTriggerTypeForStatsd(@NonNull String triggerType) {
+ switch(triggerType) {
+ case TRIGGER_TYPE_ALARM:
+ return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_ALARM;
+ case TRIGGER_TYPE_PUSH_MESSAGE:
+ return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_PUSH_MESSAGE;
+ case TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA:
+ return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_PUSH_MESSAGE_OVER_QUOTA;
+ case TRIGGER_TYPE_JOB:
+ return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_JOB;
+ default:
+ return PROCESS_START_TIME__TRIGGER_TYPE__TRIGGER_TYPE_UNKNOWN;
}
}
}
diff --git a/services/core/java/com/android/server/om/IdmapDaemon.java b/services/core/java/com/android/server/om/IdmapDaemon.java
index 8e944b7..39d1188 100644
--- a/services/core/java/com/android/server/om/IdmapDaemon.java
+++ b/services/core/java/com/android/server/om/IdmapDaemon.java
@@ -217,6 +217,7 @@
synchronized List<FabricatedOverlayInfo> getFabricatedOverlayInfos() {
final ArrayList<FabricatedOverlayInfo> allInfos = new ArrayList<>();
Connection c = null;
+ int iteratorId = -1;
try {
c = connect();
final IIdmap2 service = c.getIdmap2();
@@ -225,9 +226,9 @@
return Collections.emptyList();
}
- service.acquireFabricatedOverlayIterator();
+ iteratorId = service.acquireFabricatedOverlayIterator();
List<FabricatedOverlayInfo> infos;
- while (!(infos = service.nextFabricatedOverlayInfos()).isEmpty()) {
+ while (!(infos = service.nextFabricatedOverlayInfos(iteratorId)).isEmpty()) {
allInfos.addAll(infos);
}
return allInfos;
@@ -235,8 +236,8 @@
Slog.wtf(TAG, "failed to get all fabricated overlays", e);
} finally {
try {
- if (c.getIdmap2() != null) {
- c.getIdmap2().releaseFabricatedOverlayIterator();
+ if (c.getIdmap2() != null && iteratorId != -1) {
+ c.getIdmap2().releaseFabricatedOverlayIterator(iteratorId);
}
} catch (RemoteException e) {
// ignore
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index 0c601bf..890c891 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -1962,10 +1962,15 @@
continue;
case TAG_SHORTCUT:
- final ShortcutInfo si = parseShortcut(parser, packageName,
- shortcutUser.getUserId(), fromBackup);
- // Don't use addShortcut(), we don't need to save the icon.
- ret.mShortcuts.put(si.getId(), si);
+ try {
+ final ShortcutInfo si = parseShortcut(parser, packageName,
+ shortcutUser.getUserId(), fromBackup);
+ // Don't use addShortcut(), we don't need to save the icon.
+ ret.mShortcuts.put(si.getId(), si);
+ } catch (Exception e) {
+ // b/246540168 malformed shortcuts should be ignored
+ Slog.e(TAG, "Failed parsing shortcut.", e);
+ }
continue;
case TAG_SHARE_TARGET:
ret.mShareTargets.add(ShareTargetInfo.loadFromXml(parser));
diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java
index 8c5f053..7d9ae87 100644
--- a/services/core/java/com/android/server/wm/AsyncRotationController.java
+++ b/services/core/java/com/android/server/wm/AsyncRotationController.java
@@ -202,8 +202,7 @@
// target windows. But the windows still need to use sync transaction to keep the appearance
// in previous rotation, so request a no-op sync to keep the state.
for (int i = mTargetWindowTokens.size() - 1; i >= 0; i--) {
- if (TransitionController.SYNC_METHOD != BLASTSyncEngine.METHOD_BLAST
- && mTargetWindowTokens.valueAt(i).mAction != Operation.ACTION_SEAMLESS) {
+ if (mTargetWindowTokens.valueAt(i).canDrawBeforeStartTransaction()) {
// Expect a screenshot layer will cover the non seamless windows.
continue;
}
@@ -489,7 +488,7 @@
return false;
}
final Operation op = mTargetWindowTokens.get(w.mToken);
- if (op == null) return false;
+ if (op == null || op.canDrawBeforeStartTransaction()) return false;
if (DEBUG) Slog.d(TAG, "handleFinishDrawing " + w);
if (op.mDrawTransaction == null) {
if (w.isClientLocal()) {
@@ -554,5 +553,14 @@
Operation(@Action int action) {
mAction = action;
}
+
+ /**
+ * Returns {@code true} if the corresponding window can draw its latest content before the
+ * start transaction of rotation transition is applied.
+ */
+ boolean canDrawBeforeStartTransaction() {
+ return TransitionController.SYNC_METHOD != BLASTSyncEngine.METHOD_BLAST
+ && mAction != ACTION_SEAMLESS;
+ }
}
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 4d37e08..ac720be 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -6007,7 +6007,11 @@
if (mFrozenDisplayId != INVALID_DISPLAY && mFrozenDisplayId == w.getDisplayId()
&& mWindowsFreezingScreen != WINDOWS_FREEZING_SCREENS_TIMEOUT) {
ProtoLog.v(WM_DEBUG_ORIENTATION, "Changing surface while display frozen: %s", w);
- w.setOrientationChanging(true);
+ // WindowsState#reportResized won't tell invisible requested window to redraw,
+ // so do not set it as changing orientation to avoid affecting draw state.
+ if (w.isVisibleRequested()) {
+ w.setOrientationChanging(true);
+ }
if (mWindowsFreezingScreen == WINDOWS_FREEZING_SCREENS_NONE) {
mWindowsFreezingScreen = WINDOWS_FREEZING_SCREENS_ACTIVE;
// XXX should probably keep timeout from
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 45d8e22..4435d62 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -732,6 +732,11 @@
assertTrue(asyncRotationController.isTargetToken(decorToken));
assertShouldFreezeInsetsPosition(asyncRotationController, statusBar, true);
+ if (TransitionController.SYNC_METHOD != BLASTSyncEngine.METHOD_BLAST) {
+ // Only seamless window syncs its draw transaction with transition.
+ assertFalse(asyncRotationController.handleFinishDrawing(statusBar, mMockT));
+ assertTrue(asyncRotationController.handleFinishDrawing(screenDecor, mMockT));
+ }
screenDecor.setOrientationChanging(false);
// Status bar finishes drawing before the start transaction. Its fade-in animation will be
// executed until the transaction is committed, so it is still in target tokens.
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index cfc0da7..9bcc136 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -731,6 +731,15 @@
assertTrue(mWm.mResizingWindows.contains(startingApp));
assertTrue(startingApp.isDrawn());
assertFalse(startingApp.getOrientationChanging());
+
+ // Even if the display is frozen, invisible requested window should not be affected.
+ startingApp.mActivityRecord.mVisibleRequested = false;
+ mWm.startFreezingDisplay(0, 0, mDisplayContent);
+ doReturn(true).when(mWm.mPolicy).isScreenOn();
+ startingApp.getWindowFrames().setInsetsChanged(true);
+ startingApp.updateResizingWindowIfNeeded();
+ assertTrue(startingApp.isDrawn());
+ assertFalse(startingApp.getOrientationChanging());
}
@UseTestDisplay(addWindows = W_ABOVE_ACTIVITY)
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt
index d11ca49..fa83f22 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt
@@ -64,7 +64,7 @@
wmHelper: WindowManagerStateHelper,
direction: Direction
): Boolean {
- val ratioForScreenBottom = 0.97
+ val ratioForScreenBottom = 0.99
val fullView = wmHelper.getWindowRegion(component)
require(!fullView.isEmpty) { "Target $component view not found." }