diff options
156 files changed, 4622 insertions, 1423 deletions
diff --git a/StubLibraries.bp b/StubLibraries.bp index 91efb05b50f4..50524998d416 100644 --- a/StubLibraries.bp +++ b/StubLibraries.bp @@ -48,7 +48,6 @@ stubs_defaults { ":opt-telephony-srcs", ":opt-net-voip-srcs", ":art-module-public-api-stubs-source", - ":conscrypt.module.public.api.stubs.source", ":android_icu4j_public_api_files", ], // TODO(b/147699819): remove below aidl includes. @@ -69,7 +68,10 @@ stubs_defaults { stubs_defaults { name: "metalava-full-api-stubs-default", defaults: ["metalava-base-api-stubs-default"], - srcs: [":framework-updatable-sources"], + srcs: [ + ":conscrypt.module.public.api.stubs.source", + ":framework-updatable-sources", + ], sdk_version: "core_platform", } diff --git a/apex/extservices/Android.bp b/apex/extservices/Android.bp index 68350afdac85..0c6c4c23dce1 100644 --- a/apex/extservices/Android.bp +++ b/apex/extservices/Android.bp @@ -21,7 +21,7 @@ apex { apex_defaults { name: "com.android.extservices-defaults", updatable: true, - min_sdk_version: "R", + min_sdk_version: "current", key: "com.android.extservices.key", certificate: ":com.android.extservices.certificate", apps: ["ExtServices"], diff --git a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java index 6d9e3eddf616..887d82c6413f 100644 --- a/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/usage/AppStandbyInternal.java @@ -45,6 +45,14 @@ public interface AppStandbyInternal { boolean idle, int bucket, int reason); /** + * Callback to inform listeners that the parole state has changed. This means apps are + * allowed to do work even if they're idle or in a low bucket. + */ + public void onParoleStateChanged(boolean isParoleOn) { + // No-op by default + } + + /** * Optional callback to inform the listener that the app has transitioned into * an active state due to user interaction. */ @@ -92,6 +100,11 @@ public interface AppStandbyInternal { boolean isAppIdleFiltered(String packageName, int appId, int userId, long elapsedRealtime); + /** + * @return true if currently app idle parole mode is on. + */ + boolean isInParole(); + int[] getIdleUidsForUser(int userId); void setAppIdleAsync(String packageName, boolean idle, int userId); diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java index 24728dd8edca..cb5cb175ff24 100644 --- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java +++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java @@ -214,8 +214,7 @@ public class AppStandbyController implements AppStandbyInternal { private AppIdleHistory mAppIdleHistory; @GuardedBy("mPackageAccessListeners") - private ArrayList<AppIdleStateChangeListener> - mPackageAccessListeners = new ArrayList<>(); + private final ArrayList<AppIdleStateChangeListener> mPackageAccessListeners = new ArrayList<>(); /** Whether we've queried the list of carrier privileged apps. */ @GuardedBy("mAppIdleLock") @@ -235,6 +234,7 @@ public class AppStandbyController implements AppStandbyInternal { static final int MSG_FORCE_IDLE_STATE = 4; static final int MSG_CHECK_IDLE_STATES = 5; static final int MSG_REPORT_CONTENT_PROVIDER_USAGE = 8; + static final int MSG_PAROLE_STATE_CHANGED = 9; static final int MSG_ONE_TIME_CHECK_IDLE_STATES = 10; /** Check the state of one app: arg1 = userId, arg2 = uid, obj = (String) packageName */ static final int MSG_CHECK_PACKAGE_IDLE_STATE = 11; @@ -390,7 +390,16 @@ public class AppStandbyController implements AppStandbyInternal { @VisibleForTesting void setAppIdleEnabled(boolean enabled) { - mAppIdleEnabled = enabled; + synchronized (mAppIdleLock) { + if (mAppIdleEnabled != enabled) { + final boolean oldParoleState = isInParole(); + mAppIdleEnabled = enabled; + if (isInParole() != oldParoleState) { + postParoleStateChanged(); + } + } + } + } @Override @@ -563,11 +572,23 @@ public class AppStandbyController implements AppStandbyInternal { if (mIsCharging != isCharging) { if (DEBUG) Slog.d(TAG, "Setting mIsCharging to " + isCharging); mIsCharging = isCharging; + postParoleStateChanged(); } } } @Override + public boolean isInParole() { + return !mAppIdleEnabled || mIsCharging; + } + + private void postParoleStateChanged() { + if (DEBUG) Slog.d(TAG, "Posting MSG_PAROLE_STATE_CHANGED"); + mHandler.removeMessages(MSG_PAROLE_STATE_CHANGED); + mHandler.sendEmptyMessage(MSG_PAROLE_STATE_CHANGED); + } + + @Override public void postCheckIdleStates(int userId) { mHandler.sendMessage(mHandler.obtainMessage(MSG_CHECK_IDLE_STATES, userId, 0)); } @@ -1502,6 +1523,15 @@ public class AppStandbyController implements AppStandbyInternal { } } + private void informParoleStateChanged() { + final boolean paroled = isInParole(); + synchronized (mPackageAccessListeners) { + for (AppIdleStateChangeListener listener : mPackageAccessListeners) { + listener.onParoleStateChanged(paroled); + } + } + } + @Override public void flushToDisk(int userId) { synchronized (mAppIdleLock) { @@ -1920,6 +1950,11 @@ public class AppStandbyController implements AppStandbyInternal { args.recycle(); break; + case MSG_PAROLE_STATE_CHANGED: + if (DEBUG) Slog.d(TAG, "Parole state: " + isInParole()); + informParoleStateChanged(); + break; + case MSG_CHECK_PACKAGE_IDLE_STATE: checkAndUpdateStandbyState((String) msg.obj, msg.arg1, msg.arg2, mInjector.elapsedRealtime()); diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto index 650545f939cc..504890f6cf52 100644 --- a/cmds/statsd/src/atoms.proto +++ b/cmds/statsd/src/atoms.proto @@ -3021,14 +3021,14 @@ message LauncherUIChanged { optional string component_name = 11; // (x, y) coordinate and the index information of the target on the container - optional int32 grid_x = 12; - optional int32 grid_y = 13; - optional int32 page_id = 14; + optional int32 grid_x = 12 [default = -1]; + optional int32 grid_y = 13 [default = -1]; + optional int32 page_id = 14 [default = -2]; // e.g., folder icon's (x, y) location and index information on the workspace - optional int32 grid_x_parent = 15; - optional int32 grid_y_parent = 16; - optional int32 page_id_parent = 17; + optional int32 grid_x_parent = 15 [default = -1]; + optional int32 grid_y_parent = 16 [default = -1]; + optional int32 page_id_parent = 17 [default = -2]; // e.g., SEARCHBOX_ALLAPPS, FOLDER_WORKSPACE optional int32 hierarchy = 18; @@ -3036,7 +3036,7 @@ message LauncherUIChanged { optional bool is_work_profile = 19; // Used to store the predicted rank of the target - optional int32 rank = 20; + optional int32 rank = 20 [default = -1]; // e.g., folderLabelState can be captured in the following two fields optional int32 from_state = 21; @@ -3044,6 +3044,9 @@ message LauncherUIChanged { // e.g., autofilled or suggested texts that are not user entered optional string edittext = 23; + + // e.g., number of contents inside a container (e.g., icons inside a folder) + optional int32 cardinality = 24; } /** @@ -3064,22 +3067,34 @@ message LauncherStaticLayout { optional string component_name = 6; // (x, y) coordinate and the index information of the target on the container - optional int32 grid_x = 7; - optional int32 grid_y = 8; - optional int32 page_id = 9; + optional int32 grid_x = 7 [default = -1]; + optional int32 grid_y = 8 [default = -1]; + optional int32 page_id = 9 [default = -2]; // e.g., folder icon's (x, y) location and index information on the workspace - optional int32 grid_x_parent = 10; - optional int32 grid_y_parent = 11; - optional int32 page_id_parent = 12; - - // e.g., WORKSPACE, HOTSEAT, FOLDER_WORKSPACE, FOLDER_HOTSEAT + // e.g., when used with widgets target, use these values for (span_x, span_y) + optional int32 grid_x_parent = 10 [default = -1]; + optional int32 grid_y_parent = 11 [default = -1]; + optional int32 page_id_parent = 12 [default = -2]; + + // UNKNOWN = 0 + // HOTSEAT = 1 + // WORKSPACE = 2 + // FOLDER_HOTSEAT = 3 + // FOLDER_WORKSPACE = 4 optional int32 hierarchy = 13; optional bool is_work_profile = 14; // e.g., PIN, WIDGET TRAY, APPS TRAY, PREDICTION optional int32 origin = 15; + + // e.g., number of icons inside a folder + optional int32 cardinality = 16; + + // e.g., (x, y) span of the widget inside homescreen grid system + optional int32 span_x = 17 [default = 1]; + optional int32 span_y = 18 [default = 1]; } /** diff --git a/cmds/statsd/src/shell/ShellSubscriber.cpp b/cmds/statsd/src/shell/ShellSubscriber.cpp index bed836a1bd90..7b687210ce33 100644 --- a/cmds/statsd/src/shell/ShellSubscriber.cpp +++ b/cmds/statsd/src/shell/ShellSubscriber.cpp @@ -19,6 +19,7 @@ #include "ShellSubscriber.h" #include <android-base/file.h> + #include "matchers/matcher_util.h" #include "stats_log_util.h" @@ -32,42 +33,53 @@ const static int FIELD_ID_ATOM = 1; void ShellSubscriber::startNewSubscription(int in, int out, int timeoutSec) { int myToken = claimToken(); + VLOG("ShellSubscriber: new subscription %d has come in", myToken); mSubscriptionShouldEnd.notify_one(); shared_ptr<SubscriptionInfo> mySubscriptionInfo = make_shared<SubscriptionInfo>(in, out); - if (!readConfig(mySubscriptionInfo)) { - return; - } + if (!readConfig(mySubscriptionInfo)) return; + + { + std::unique_lock<std::mutex> lock(mMutex); + if (myToken != mToken) { + // Some other subscription has already come in. Stop. + return; + } + mSubscriptionInfo = mySubscriptionInfo; + + spawnHelperThreadsLocked(mySubscriptionInfo, myToken); + waitForSubscriptionToEndLocked(mySubscriptionInfo, myToken, lock, timeoutSec); + + if (mSubscriptionInfo == mySubscriptionInfo) { + mSubscriptionInfo = nullptr; + } - // critical-section - std::unique_lock<std::mutex> lock(mMutex); - if (myToken != mToken) { - // Some other subscription has already come in. Stop. - return; } - mSubscriptionInfo = mySubscriptionInfo; +} - if (mySubscriptionInfo->mPulledInfo.size() > 0 && mySubscriptionInfo->mPullIntervalMin > 0) { - // This thread terminates after it detects that mToken has changed. +void ShellSubscriber::spawnHelperThreadsLocked(shared_ptr<SubscriptionInfo> myInfo, int myToken) { + if (!myInfo->mPulledInfo.empty() && myInfo->mPullIntervalMin > 0) { std::thread puller([this, myToken] { startPull(myToken); }); puller.detach(); } - // Block until subscription has ended. + std::thread heartbeatSender([this, myToken] { sendHeartbeats(myToken); }); + heartbeatSender.detach(); +} + +void ShellSubscriber::waitForSubscriptionToEndLocked(shared_ptr<SubscriptionInfo> myInfo, + int myToken, + std::unique_lock<std::mutex>& lock, + int timeoutSec) { if (timeoutSec > 0) { - mSubscriptionShouldEnd.wait_for( - lock, timeoutSec * 1s, [this, myToken, &mySubscriptionInfo] { - return mToken != myToken || !mySubscriptionInfo->mClientAlive; - }); + mSubscriptionShouldEnd.wait_for(lock, timeoutSec * 1s, [this, myToken, &myInfo] { + return mToken != myToken || !myInfo->mClientAlive; + }); } else { - mSubscriptionShouldEnd.wait(lock, [this, myToken, &mySubscriptionInfo] { - return mToken != myToken || !mySubscriptionInfo->mClientAlive; + mSubscriptionShouldEnd.wait(lock, [this, myToken, &myInfo] { + return mToken != myToken || !myInfo->mClientAlive; }); } - - if (mSubscriptionInfo == mySubscriptionInfo) { - mSubscriptionInfo = nullptr; - } } // Atomically claim the next token. Token numbers denote subscriber ordering. @@ -129,51 +141,55 @@ bool ShellSubscriber::readConfig(shared_ptr<SubscriptionInfo> subscriptionInfo) return true; } -void ShellSubscriber::startPull(int64_t myToken) { +void ShellSubscriber::startPull(int myToken) { + VLOG("ShellSubscriber: pull thread %d starting", myToken); while (true) { - std::lock_guard<std::mutex> lock(mMutex); - if (!mSubscriptionInfo || mToken != myToken) { - VLOG("Pulling thread %lld done!", (long long)myToken); - return; - } + { + std::lock_guard<std::mutex> lock(mMutex); + if (!mSubscriptionInfo || mToken != myToken) { + VLOG("ShellSubscriber: pulling thread %d done!", myToken); + return; + } - int64_t nowMillis = getElapsedRealtimeMillis(); - for (auto& pullInfo : mSubscriptionInfo->mPulledInfo) { - if (pullInfo.mPrevPullElapsedRealtimeMs + pullInfo.mInterval < nowMillis) { - vector<std::shared_ptr<LogEvent>> data; - vector<int32_t> uids; - uids.insert(uids.end(), pullInfo.mPullUids.begin(), pullInfo.mPullUids.end()); - // This is slow. Consider storing the uids per app and listening to uidmap updates. - for (const string& pkg : pullInfo.mPullPackages) { - set<int32_t> uidsForPkg = mUidMap->getAppUid(pkg); - uids.insert(uids.end(), uidsForPkg.begin(), uidsForPkg.end()); + int64_t nowMillis = getElapsedRealtimeMillis(); + for (PullInfo& pullInfo : mSubscriptionInfo->mPulledInfo) { + if (pullInfo.mPrevPullElapsedRealtimeMs + pullInfo.mInterval >= nowMillis) { + continue; } - uids.push_back(DEFAULT_PULL_UID); + + vector<int32_t> uids; + getUidsForPullAtom(&uids, pullInfo); + + vector<std::shared_ptr<LogEvent>> data; mPullerMgr->Pull(pullInfo.mPullerMatcher.atom_id(), uids, &data); - VLOG("pulled %zu atoms with id %d", data.size(), pullInfo.mPullerMatcher.atom_id()); + VLOG("Pulled %zu atoms with id %d", data.size(), pullInfo.mPullerMatcher.atom_id()); + writePulledAtomsLocked(data, pullInfo.mPullerMatcher); - if (!writePulledAtomsLocked(data, pullInfo.mPullerMatcher)) { - mSubscriptionInfo->mClientAlive = false; - mSubscriptionShouldEnd.notify_one(); - return; - } pullInfo.mPrevPullElapsedRealtimeMs = nowMillis; } } - VLOG("Pulling thread %lld sleep....", (long long)myToken); + VLOG("ShellSubscriber: pulling thread %d sleeping for %d ms", myToken, + mSubscriptionInfo->mPullIntervalMin); std::this_thread::sleep_for(std::chrono::milliseconds(mSubscriptionInfo->mPullIntervalMin)); } } -// \return boolean indicating if writes were successful (will return false if -// client dies) -bool ShellSubscriber::writePulledAtomsLocked(const vector<std::shared_ptr<LogEvent>>& data, +void ShellSubscriber::getUidsForPullAtom(vector<int32_t>* uids, const PullInfo& pullInfo) { + uids->insert(uids->end(), pullInfo.mPullUids.begin(), pullInfo.mPullUids.end()); + // This is slow. Consider storing the uids per app and listening to uidmap updates. + for (const string& pkg : pullInfo.mPullPackages) { + set<int32_t> uidsForPkg = mUidMap->getAppUid(pkg); + uids->insert(uids->end(), uidsForPkg.begin(), uidsForPkg.end()); + } + uids->push_back(DEFAULT_PULL_UID); +} + +void ShellSubscriber::writePulledAtomsLocked(const vector<std::shared_ptr<LogEvent>>& data, const SimpleAtomMatcher& matcher) { mProto.clear(); int count = 0; for (const auto& event : data) { - VLOG("%s", event->ToString().c_str()); if (matchesSimple(*mUidMap, matcher, *event)) { count++; uint64_t atomToken = mProto.start(util::FIELD_TYPE_MESSAGE | @@ -183,55 +199,67 @@ bool ShellSubscriber::writePulledAtomsLocked(const vector<std::shared_ptr<LogEve } } - if (count > 0) { - // First write the payload size. - size_t bufferSize = mProto.size(); - if (!android::base::WriteFully(mSubscriptionInfo->mOutputFd, &bufferSize, - sizeof(bufferSize))) { - return false; - } - - VLOG("%d atoms, proto size: %zu", count, bufferSize); - // Then write the payload. - if (!mProto.flush(mSubscriptionInfo->mOutputFd)) { - return false; - } - } - - return true; + if (count > 0) attemptWriteToSocketLocked(mProto.size()); } void ShellSubscriber::onLogEvent(const LogEvent& event) { std::lock_guard<std::mutex> lock(mMutex); - if (!mSubscriptionInfo) { - return; - } + if (!mSubscriptionInfo) return; mProto.clear(); for (const auto& matcher : mSubscriptionInfo->mPushedMatchers) { if (matchesSimple(*mUidMap, matcher, event)) { - VLOG("%s", event.ToString().c_str()); uint64_t atomToken = mProto.start(util::FIELD_TYPE_MESSAGE | util::FIELD_COUNT_REPEATED | FIELD_ID_ATOM); event.ToProto(mProto); mProto.end(atomToken); + attemptWriteToSocketLocked(mProto.size()); + } + } +} - // First write the payload size. - size_t bufferSize = mProto.size(); - if (!android::base::WriteFully(mSubscriptionInfo->mOutputFd, &bufferSize, - sizeof(bufferSize))) { - mSubscriptionInfo->mClientAlive = false; - mSubscriptionShouldEnd.notify_one(); +// Tries to write the atom encoded in mProto to the socket. If the write fails +// because the read end of the pipe has closed, signals to other threads that +// the subscription should end. +void ShellSubscriber::attemptWriteToSocketLocked(size_t dataSize) { + // First write the payload size. + if (!android::base::WriteFully(mSubscriptionInfo->mOutputFd, &dataSize, sizeof(dataSize))) { + mSubscriptionInfo->mClientAlive = false; + mSubscriptionShouldEnd.notify_one(); + return; + } + + if (dataSize == 0) return; + + // Then, write the payload. + if (!mProto.flush(mSubscriptionInfo->mOutputFd)) { + mSubscriptionInfo->mClientAlive = false; + mSubscriptionShouldEnd.notify_one(); + return; + } + + mLastWriteMs = getElapsedRealtimeMillis(); +} + +// Send a heartbeat, consisting solely of a data size of 0, if perfd has not +// recently received any writes from statsd. When it receives the data size of +// 0, perfd will not expect any data and recheck whether the shell command is +// still running. +void ShellSubscriber::sendHeartbeats(int myToken) { + while (true) { + { + std::lock_guard<std::mutex> lock(mMutex); + if (!mSubscriptionInfo || myToken != mToken) { + VLOG("ShellSubscriber: heartbeat thread %d done!", myToken); return; } - // Then write the payload. - if (!mProto.flush(mSubscriptionInfo->mOutputFd)) { - mSubscriptionInfo->mClientAlive = false; - mSubscriptionShouldEnd.notify_one(); - return; + if (getElapsedRealtimeMillis() - mLastWriteMs > kMsBetweenHeartbeats) { + VLOG("ShellSubscriber: sending a heartbeat to perfd"); + attemptWriteToSocketLocked(/*dataSize=*/0); } } + std::this_thread::sleep_for(std::chrono::milliseconds(kMsBetweenHeartbeats)); } } diff --git a/cmds/statsd/src/shell/ShellSubscriber.h b/cmds/statsd/src/shell/ShellSubscriber.h index 61457d89f224..26c8a2a0b683 100644 --- a/cmds/statsd/src/shell/ShellSubscriber.h +++ b/cmds/statsd/src/shell/ShellSubscriber.h @@ -38,11 +38,11 @@ namespace statsd { * * A shell subscription lasts *until shell exits*. Unlike config based clients, a shell client * communicates with statsd via file descriptors. They can subscribe pushed and pulled atoms. - * The atoms are sent back to the client in real time, as opposed to - * keeping the data in memory. Shell clients do not subscribe aggregated metrics, as they are - * responsible for doing the aggregation after receiving the atom events. + * The atoms are sent back to the client in real time, as opposed to keeping the data in memory. + * Shell clients do not subscribe aggregated metrics, as they are responsible for doing the + * aggregation after receiving the atom events. * - * Shell client pass ShellSubscription in the proto binary format. Client can update the + * Shell clients pass ShellSubscription in the proto binary format. Clients can update the * subscription by sending a new subscription. The new subscription would replace the old one. * Input data stream format is: * @@ -54,7 +54,7 @@ namespace statsd { * The stream would be in the following format: * |size_t|shellData proto|size_t|shellData proto|.... * - * Only one shell subscriber allowed at a time, because each shell subscriber blocks one thread + * Only one shell subscriber is allowed at a time because each shell subscriber blocks one thread * until it exits. */ class ShellSubscriber : public virtual RefBase { @@ -100,11 +100,28 @@ private: bool readConfig(std::shared_ptr<SubscriptionInfo> subscriptionInfo); - void startPull(int64_t myToken); + void spawnHelperThreadsLocked(std::shared_ptr<SubscriptionInfo> myInfo, int myToken); - bool writePulledAtomsLocked(const vector<std::shared_ptr<LogEvent>>& data, + void waitForSubscriptionToEndLocked(std::shared_ptr<SubscriptionInfo> myInfo, + int myToken, + std::unique_lock<std::mutex>& lock, + int timeoutSec); + + void startPull(int myToken); + + void writePulledAtomsLocked(const vector<std::shared_ptr<LogEvent>>& data, const SimpleAtomMatcher& matcher); + void getUidsForPullAtom(vector<int32_t>* uids, const PullInfo& pullInfo); + + void attemptWriteToSocketLocked(size_t dataSize); + + // Send ocassional heartbeats for two reasons: (a) for statsd to detect when + // the read end of the pipe has closed and (b) for perfd to escape a + // blocking read call and recheck if the user has terminated the + // subscription. + void sendHeartbeats(int myToken); + sp<UidMap> mUidMap; sp<StatsPullerManager> mPullerMgr; @@ -120,6 +137,11 @@ private: int mToken = 0; const int32_t DEFAULT_PULL_UID = AID_SYSTEM; + + // Tracks when we last send data to perfd. We need that time to determine + // when next to send a heartbeat. + int64_t mLastWriteMs = 0; + const int64_t kMsBetweenHeartbeats = 1000; }; } // namespace statsd diff --git a/cmds/statsd/tests/shell/ShellSubscriber_test.cpp b/cmds/statsd/tests/shell/ShellSubscriber_test.cpp index 7b952d7a392e..363fcb4bf193 100644 --- a/cmds/statsd/tests/shell/ShellSubscriber_test.cpp +++ b/cmds/statsd/tests/shell/ShellSubscriber_test.cpp @@ -86,28 +86,34 @@ void runShellTest(ShellSubscription config, sp<MockUidMap> uidMap, // wait for the data to be written. std::this_thread::sleep_for(100ms); - int expected_data_size = expectedData.ByteSize(); - - // now read from the pipe. firstly read the atom size. - size_t dataSize = 0; - EXPECT_EQ((int)sizeof(dataSize), read(fds_data[0], &dataSize, sizeof(dataSize))); - - EXPECT_EQ(expected_data_size, (int)dataSize); - - // then read that much data which is the atom in proto binary format - vector<uint8_t> dataBuffer(dataSize); - EXPECT_EQ((int)dataSize, read(fds_data[0], dataBuffer.data(), dataSize)); - - // make sure the received bytes can be parsed to an atom - ShellData receivedAtom; - EXPECT_TRUE(receivedAtom.ParseFromArray(dataBuffer.data(), dataSize) != 0); + // Because we might receive heartbeats from statsd, consisting of data sizes + // of 0, encapsulate reads within a while loop. + bool readAtom = false; + while (!readAtom) { + // Read the atom size. + size_t dataSize = 0; + read(fds_data[0], &dataSize, sizeof(dataSize)); + if (dataSize == 0) continue; + EXPECT_EQ(expectedData.ByteSize(), int(dataSize)); + + // Read that much data in proto binary format. + vector<uint8_t> dataBuffer(dataSize); + EXPECT_EQ((int)dataSize, read(fds_data[0], dataBuffer.data(), dataSize)); + + // Make sure the received bytes can be parsed to an atom. + ShellData receivedAtom; + EXPECT_TRUE(receivedAtom.ParseFromArray(dataBuffer.data(), dataSize) != 0); + + // Serialize the expected atom to byte array and compare to make sure + // they are the same. + vector<uint8_t> expectedAtomBuffer(expectedData.ByteSize()); + expectedData.SerializeToArray(expectedAtomBuffer.data(), expectedData.ByteSize()); + EXPECT_EQ(expectedAtomBuffer, dataBuffer); + + readAtom = true; + } - // serialze the expected atom to bytes. and compare. to make sure they are the same. - vector<uint8_t> atomBuffer(expected_data_size); - expectedData.SerializeToArray(&atomBuffer[0], expected_data_size); - EXPECT_EQ(atomBuffer, dataBuffer); close(fds_data[0]); - if (reader.joinable()) { reader.join(); } diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 187274a837a0..7912dacac377 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -635,10 +635,11 @@ public class UserManager { /** * Specifies if a user is disallowed from adding new users. This can only be set by device - * owners, profile owners on the primary user or profile owners of organization-owned managed - * profiles on the parent profile. The default value is <code>false</code>. + * owners or profile owners on the primary user. The default value is <code>false</code>. * <p>This restriction has no effect on secondary users and managed profiles since only the * primary user can add other users. + * <p> When the device is an organization-owned device provisioned with a managed profile, + * this restriction will be set as a base restriction which cannot be removed by any admin. * * <p>Key for user restrictions. * <p>Type: Boolean diff --git a/core/java/android/permission/PermissionControllerManager.java b/core/java/android/permission/PermissionControllerManager.java index ed429dd835c3..06caa03e3cb4 100644 --- a/core/java/android/permission/PermissionControllerManager.java +++ b/core/java/android/permission/PermissionControllerManager.java @@ -56,10 +56,15 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.infra.AndroidFuture; import com.android.internal.infra.RemoteStream; import com.android.internal.infra.ServiceConnector; +import com.android.internal.os.TransferPipe; import com.android.internal.util.CollectionUtils; import libcore.util.EmptyArray; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -67,7 +72,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** @@ -476,6 +483,36 @@ public final class PermissionControllerManager { } /** + * Dump permission controller state. + * + * @hide + */ + public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @Nullable String[] args) { + CompletableFuture<Throwable> dumpResult = new CompletableFuture<>(); + mRemoteService.postForResult( + service -> TransferPipe.dumpAsync(service.asBinder(), args)) + .whenComplete( + (dump, err) -> { + try (FileOutputStream out = new FileOutputStream(fd)) { + out.write(dump); + } catch (IOException | NullPointerException e) { + Log.e(TAG, "Could for forwards permission controller dump", e); + } + + dumpResult.complete(err); + }); + + try { + Throwable err = dumpResult.get(UNBIND_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + if (err != null) { + throw err; + } + } catch (Throwable e) { + Log.e(TAG, "Could not dump permission controller state", e); + } + } + + /** * Gets the runtime permissions for an app. * * @param packageName The package for which to query. diff --git a/core/java/android/permission/PermissionControllerService.java b/core/java/android/permission/PermissionControllerService.java index 82a7d788100d..c6ede32d0864 100644 --- a/core/java/android/permission/PermissionControllerService.java +++ b/core/java/android/permission/PermissionControllerService.java @@ -50,9 +50,11 @@ import com.android.internal.infra.AndroidFuture; import com.android.internal.util.CollectionUtils; import com.android.internal.util.Preconditions; +import java.io.FileDescriptor; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -494,6 +496,11 @@ public abstract class PermissionControllerService extends Service { "packageName cannot be null"); onOneTimePermissionSessionTimeout(packageName); } + + @Override + protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + PermissionControllerService.this.dump(fd, writer, args); + } }; } } diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java index 2e00c0c9d2a4..327bca268a7b 100644 --- a/core/java/android/provider/DocumentsProvider.java +++ b/core/java/android/provider/DocumentsProvider.java @@ -1274,8 +1274,6 @@ public abstract class DocumentsProvider extends ContentProvider { out.putParcelable(DocumentsContract.EXTRA_RESULT, path); } else if (METHOD_GET_DOCUMENT_METADATA.equals(method)) { - enforceReadPermissionInner(documentUri, getCallingPackage(), - getCallingAttributionTag(), null); return getDocumentMetadata(documentId); } else { throw new UnsupportedOperationException("Method not supported " + method); diff --git a/core/java/android/service/autofill/IInlineSuggestionUi.aidl b/core/java/android/service/autofill/IInlineSuggestionUi.aidl new file mode 100644 index 000000000000..7289853064f8 --- /dev/null +++ b/core/java/android/service/autofill/IInlineSuggestionUi.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +import android.service.autofill.ISurfacePackageResultCallback; + +/** + * Interface to interact with a remote inline suggestion UI. + * + * @hide + */ +oneway interface IInlineSuggestionUi { + void getSurfacePackage(ISurfacePackageResultCallback callback); + void releaseSurfaceControlViewHost(); +} diff --git a/core/java/android/service/autofill/IInlineSuggestionUiCallback.aidl b/core/java/android/service/autofill/IInlineSuggestionUiCallback.aidl index 172cfef9fee2..97eb790b9acc 100644 --- a/core/java/android/service/autofill/IInlineSuggestionUiCallback.aidl +++ b/core/java/android/service/autofill/IInlineSuggestionUiCallback.aidl @@ -18,17 +18,19 @@ package android.service.autofill; import android.content.IntentSender; import android.os.IBinder; +import android.service.autofill.IInlineSuggestionUi; import android.view.SurfaceControlViewHost; /** - * Interface to receive events from inline suggestions. + * Interface to receive events from a remote inline suggestion UI. * * @hide */ oneway interface IInlineSuggestionUiCallback { void onClick(); void onLongClick(); - void onContent(in SurfaceControlViewHost.SurfacePackage surface, int width, int height); + void onContent(in IInlineSuggestionUi content, in SurfaceControlViewHost.SurfacePackage surface, + int width, int height); void onError(); void onTransferTouchFocusToImeWindow(in IBinder sourceInputToken, int displayId); void onStartIntentSender(in IntentSender intentSender); diff --git a/core/java/android/service/autofill/ISurfacePackageResultCallback.aidl b/core/java/android/service/autofill/ISurfacePackageResultCallback.aidl new file mode 100644 index 000000000000..0c2c624952eb --- /dev/null +++ b/core/java/android/service/autofill/ISurfacePackageResultCallback.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.autofill; + +import android.view.SurfaceControlViewHost; + +/** + * Interface to receive a SurfaceControlViewHost.SurfacePackage. + * + * @hide + */ +oneway interface ISurfacePackageResultCallback { + void onResult(in SurfaceControlViewHost.SurfacePackage result); +} diff --git a/core/java/android/service/autofill/InlineSuggestionRenderService.java b/core/java/android/service/autofill/InlineSuggestionRenderService.java index 6c22b1936d74..3ea443bab3f8 100644 --- a/core/java/android/service/autofill/InlineSuggestionRenderService.java +++ b/core/java/android/service/autofill/InlineSuggestionRenderService.java @@ -33,6 +33,7 @@ import android.os.Looper; import android.os.RemoteCallback; import android.os.RemoteException; import android.util.Log; +import android.util.LruCache; import android.util.Size; import android.view.Display; import android.view.SurfaceControlViewHost; @@ -40,6 +41,8 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import java.lang.ref.WeakReference; + /** * A service that renders an inline presentation view given the {@link InlinePresentation}. * @@ -65,6 +68,27 @@ public abstract class InlineSuggestionRenderService extends Service { private IInlineSuggestionUiCallback mCallback; + + /** + * A local LRU cache keeping references to the inflated {@link SurfaceControlViewHost}s, so + * they can be released properly when no longer used. Each view needs to be tracked separately, + * therefore for simplicity we use the hash code of the value object as key in the cache. + */ + private final LruCache<InlineSuggestionUiImpl, Boolean> mActiveInlineSuggestions = + new LruCache<InlineSuggestionUiImpl, Boolean>(30) { + @Override + public void entryRemoved(boolean evicted, InlineSuggestionUiImpl key, + Boolean oldValue, + Boolean newValue) { + if (evicted) { + Log.w(TAG, + "Hit max=100 entries in the cache. Releasing oldest one to make " + + "space."); + key.releaseSurfaceControlViewHost(); + } + } + }; + /** * If the specified {@code width}/{@code height} is an exact value, then it will be returned as * is, otherwise the method tries to measure a size that is just large enough to fit the view @@ -169,8 +193,14 @@ public abstract class InlineSuggestionRenderService extends Service { return true; }); - sendResult(callback, host.getSurfacePackage(), measuredSize.getWidth(), - measuredSize.getHeight()); + try { + InlineSuggestionUiImpl uiImpl = new InlineSuggestionUiImpl(host, mHandler); + mActiveInlineSuggestions.put(uiImpl, true); + callback.onContent(new InlineSuggestionUiWrapper(uiImpl), host.getSurfacePackage(), + measuredSize.getWidth(), measuredSize.getHeight()); + } catch (RemoteException e) { + Log.w(TAG, "RemoteException calling onContent()"); + } } finally { updateDisplay(Display.DEFAULT_DISPLAY); } @@ -181,12 +211,87 @@ public abstract class InlineSuggestionRenderService extends Service { callback.sendResult(rendererInfo); } - private void sendResult(@NonNull IInlineSuggestionUiCallback callback, - @Nullable SurfaceControlViewHost.SurfacePackage surface, int width, int height) { - try { - callback.onContent(surface, width, height); - } catch (RemoteException e) { - Log.w(TAG, "RemoteException calling onContent(" + surface + ")"); + /** + * A wrapper class around the {@link InlineSuggestionUiImpl} to ensure it's not strongly + * reference by the remote system server process. + */ + private static final class InlineSuggestionUiWrapper extends + android.service.autofill.IInlineSuggestionUi.Stub { + + private final WeakReference<InlineSuggestionUiImpl> mUiImpl; + + InlineSuggestionUiWrapper(InlineSuggestionUiImpl uiImpl) { + mUiImpl = new WeakReference<>(uiImpl); + } + + @Override + public void releaseSurfaceControlViewHost() { + final InlineSuggestionUiImpl uiImpl = mUiImpl.get(); + if (uiImpl != null) { + uiImpl.releaseSurfaceControlViewHost(); + } + } + + @Override + public void getSurfacePackage(ISurfacePackageResultCallback callback) { + final InlineSuggestionUiImpl uiImpl = mUiImpl.get(); + if (uiImpl != null) { + uiImpl.getSurfacePackage(callback); + } + } + } + + /** + * Keeps track of a SurfaceControlViewHost to ensure it's released when its lifecycle ends. + * + * <p>This class is thread safe, because all the outside calls are piped into a single + * handler thread to be processed. + */ + private final class InlineSuggestionUiImpl { + + @Nullable + private SurfaceControlViewHost mViewHost; + @NonNull + private final Handler mHandler; + + InlineSuggestionUiImpl(SurfaceControlViewHost viewHost, Handler handler) { + this.mViewHost = viewHost; + this.mHandler = handler; + } + + /** + * Call {@link SurfaceControlViewHost#release()} to release it. After this, this view is + * not usable, and any further calls to the + * {@link #getSurfacePackage(ISurfacePackageResultCallback)} will get {@code null} result. + */ + public void releaseSurfaceControlViewHost() { + mHandler.post(() -> { + if (mViewHost == null) { + return; + } + Log.v(TAG, "Releasing inline suggestion view host"); + mViewHost.release(); + mViewHost = null; + InlineSuggestionRenderService.this.mActiveInlineSuggestions.remove( + InlineSuggestionUiImpl.this); + Log.v(TAG, "Removed the inline suggestion from the cache, current size=" + + InlineSuggestionRenderService.this.mActiveInlineSuggestions.size()); + }); + } + + /** + * Sends back a new {@link android.view.SurfaceControlViewHost.SurfacePackage} if the view + * is not released, {@code null} otherwise. + */ + public void getSurfacePackage(ISurfacePackageResultCallback callback) { + Log.d(TAG, "getSurfacePackage"); + mHandler.post(() -> { + try { + callback.onResult(mViewHost == null ? null : mViewHost.getSurfacePackage()); + } catch (RemoteException e) { + Log.w(TAG, "RemoteException calling onSurfacePackage"); + } + }); } } diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java index 4e9e3d49694e..d2dfb29ba25c 100644 --- a/core/java/android/service/dreams/DreamService.java +++ b/core/java/android/service/dreams/DreamService.java @@ -1075,8 +1075,12 @@ public class DreamService extends Service implements Window.Callback { @Override public void onViewDetachedFromWindow(View v) { - mActivity = null; - finish(); + if (mActivity == null || !mActivity.isChangingConfigurations()) { + // Only stop the dream if the view is not detached by relaunching + // activity for configuration changes. + mActivity = null; + finish(); + } } }); } diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index a135b0ca148b..fcc4a6ec4d92 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -1117,7 +1117,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation * Cancel on-going animation to show/hide {@link InsetsType}. */ @VisibleForTesting - public void cancelExistingAnimation() { + public void cancelExistingAnimations() { cancelExistingControllers(all()); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 42f11c162473..a17af6c90617 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -4624,6 +4624,8 @@ public final class ViewRootImpl implements ViewParent, setAccessibilityFocus(null, null); + mInsetsController.cancelExistingAnimations(); + mView.assignParent(null); mView = null; mAttachInfo.mRootView = null; diff --git a/core/java/android/view/ViewRootInsetsControllerHost.java b/core/java/android/view/ViewRootInsetsControllerHost.java index d8bf58f78339..9674a80c8159 100644 --- a/core/java/android/view/ViewRootInsetsControllerHost.java +++ b/core/java/android/view/ViewRootInsetsControllerHost.java @@ -104,10 +104,10 @@ public class ViewRootInsetsControllerHost implements InsetsController.Host { @Override public void applySurfaceParams(SyncRtSurfaceTransactionApplier.SurfaceParams... params) { + if (mViewRoot.mView == null) { + throw new IllegalStateException("View of the ViewRootImpl is not initiated."); + } if (mApplier == null) { - if (mViewRoot.mView == null) { - throw new IllegalStateException("View of the ViewRootImpl is not initiated."); - } mApplier = new SyncRtSurfaceTransactionApplier(mViewRoot.mView); } if (mViewRoot.mView.isHardwareAccelerated()) { diff --git a/core/java/android/view/inputmethod/InlineSuggestion.java b/core/java/android/view/inputmethod/InlineSuggestion.java index 6b1a480986c8..4c72474435a4 100644 --- a/core/java/android/view/inputmethod/InlineSuggestion.java +++ b/core/java/android/view/inputmethod/InlineSuggestion.java @@ -18,11 +18,13 @@ package android.view.inputmethod; import android.annotation.BinderThread; import android.annotation.CallbackExecutor; +import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.content.Context; -import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; @@ -42,26 +44,26 @@ import java.util.concurrent.Executor; import java.util.function.Consumer; /** - * This class represents an inline suggestion which is made by one app - * and can be embedded into the UI of another. Suggestions may contain - * sensitive information not known to the host app which needs to be - * protected from spoofing. To address that the suggestion view inflated - * on demand for embedding is created in such a way that the hosting app - * cannot introspect its content and cannot interact with it. + * This class represents an inline suggestion which is made by one app and can be embedded into the + * UI of another. Suggestions may contain sensitive information not known to the host app which + * needs to be protected from spoofing. To address that the suggestion view inflated on demand for + * embedding is created in such a way that the hosting app cannot introspect its content and cannot + * interact with it. */ -@DataClass( - genEqualsHashCode = true, - genToString = true, - genHiddenConstDefs = true, +@DataClass(genEqualsHashCode = true, genToString = true, genHiddenConstDefs = true, genHiddenConstructor = true) -@DataClass.Suppress({"getContentProvider"}) public final class InlineSuggestion implements Parcelable { private static final String TAG = "InlineSuggestion"; - private final @NonNull InlineSuggestionInfo mInfo; + @NonNull + private final InlineSuggestionInfo mInfo; - private final @Nullable IInlineContentProvider mContentProvider; + /** + * @hide + */ + @Nullable + private final IInlineContentProvider mContentProvider; /** * Used to keep a strong reference to the callback so it doesn't get garbage collected. @@ -69,7 +71,8 @@ public final class InlineSuggestion implements Parcelable { * @hide */ @DataClass.ParcelWith(InlineContentCallbackImplParceling.class) - private @Nullable InlineContentCallbackImpl mInlineContentCallback; + @Nullable + private InlineContentCallbackImpl mInlineContentCallback; /** * Creates a new {@link InlineSuggestion}, for testing purpose. @@ -87,8 +90,7 @@ public final class InlineSuggestion implements Parcelable { * * @hide */ - public InlineSuggestion( - @NonNull InlineSuggestionInfo info, + public InlineSuggestion(@NonNull InlineSuggestionInfo info, @Nullable IInlineContentProvider contentProvider) { this(info, contentProvider, /* inlineContentCallback */ null); } @@ -96,25 +98,30 @@ public final class InlineSuggestion implements Parcelable { /** * Inflates a view with the content of this suggestion at a specific size. * - * <p> The size must be either 1) between the - * {@link android.widget.inline.InlinePresentationSpec#getMinSize() min size} and the - * {@link android.widget.inline.InlinePresentationSpec#getMaxSize() max size} of the - * presentation spec returned by {@link InlineSuggestionInfo#getInlinePresentationSpec()}, - * or 2) {@link ViewGroup.LayoutParams#WRAP_CONTENT}. If the size is set to - * {@link ViewGroup.LayoutParams#WRAP_CONTENT}, then the size of the inflated view will be just - * large enough to fit the content, while still conforming to the min / max size specified by - * the {@link android.widget.inline.InlinePresentationSpec}. + * <p> Each dimension of the size must satisfy one of the following conditions: + * + * <ol> + * <li>between {@link android.widget.inline.InlinePresentationSpec#getMinSize()} and + * {@link android.widget.inline.InlinePresentationSpec#getMaxSize()} of the presentation spec + * from {@code mInfo} + * <li>{@link ViewGroup.LayoutParams#WRAP_CONTENT} + * </ol> + * + * If the size is set to {@link + * ViewGroup.LayoutParams#WRAP_CONTENT}, then the size of the inflated view will be just large + * enough to fit the content, while still conforming to the min / max size specified by the + * {@link android.widget.inline.InlinePresentationSpec}. * * <p> The caller can attach an {@link android.view.View.OnClickListener} and/or an - * {@link android.view.View.OnLongClickListener} to the view in the - * {@code callback} to receive click and long click events on the view. + * {@link android.view.View.OnLongClickListener} to the view in the {@code callback} to receive + * click and long click events on the view. * * @param context Context in which to inflate the view. - * @param size The size at which to inflate the suggestion. For each dimension, it maybe - * an exact value or {@link ViewGroup.LayoutParams#WRAP_CONTENT}. - * @param callback Callback for receiving the inflated view, where the - * {@link ViewGroup.LayoutParams} of the view is set as the actual size of - * the underlying remote view. + * @param size The size at which to inflate the suggestion. For each dimension, it maybe an + * exact value or {@link ViewGroup.LayoutParams#WRAP_CONTENT}. + * @param callback Callback for receiving the inflated view, where the {@link + * ViewGroup.LayoutParams} of the view is set as the actual size of the + * underlying remote view. * @throws IllegalArgumentException If an invalid argument is passed. * @throws IllegalStateException If this method is already called. */ @@ -130,19 +137,17 @@ public final class InlineSuggestion implements Parcelable { + ", nor wrap_content"); } mInlineContentCallback = getInlineContentCallback(context, callbackExecutor, callback); - AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { - if (mContentProvider == null) { - callback.accept(/* view */ null); - return; - } - try { - mContentProvider.provideContent(size.getWidth(), size.getHeight(), - new InlineContentCallbackWrapper(mInlineContentCallback)); - } catch (RemoteException e) { - Slog.w(TAG, "Error creating suggestion content surface: " + e); - callback.accept(/* view */ null); - } - }); + if (mContentProvider == null) { + callbackExecutor.execute(() -> callback.accept(/* view */ null)); + return; + } + try { + mContentProvider.provideContent(size.getWidth(), size.getHeight(), + new InlineContentCallbackWrapper(mInlineContentCallback)); + } catch (RemoteException e) { + Slog.w(TAG, "Error creating suggestion content surface: " + e); + callbackExecutor.execute(() -> callback.accept(/* view */ null)); + } } /** @@ -161,9 +166,14 @@ public final class InlineSuggestion implements Parcelable { if (mInlineContentCallback != null) { throw new IllegalStateException("Already called #inflate()"); } - return new InlineContentCallbackImpl(context, callbackExecutor, callback); + return new InlineContentCallbackImpl(context, mContentProvider, callbackExecutor, + callback); } + /** + * A wrapper class around the {@link InlineContentCallbackImpl} to ensure it's not strongly + * reference by the remote system server process. + */ private static final class InlineContentCallbackWrapper extends IInlineContentCallback.Stub { private final WeakReference<InlineContentCallbackImpl> mCallbackImpl; @@ -201,17 +211,68 @@ public final class InlineSuggestion implements Parcelable { } } + /** + * Handles the communication between the inline suggestion view in current (IME) process and + * the remote view provided from the system server. + * + * <p>This class is thread safe, because all the outside calls are piped into a single + * handler thread to be processed. + */ private static final class InlineContentCallbackImpl { - private final @NonNull Context mContext; - private final @NonNull Executor mCallbackExecutor; - private final @NonNull Consumer<InlineContentView> mCallback; - private @Nullable InlineContentView mView; + @NonNull + private final Handler mMainHandler = new Handler(Looper.getMainLooper()); + + @NonNull + private final Context mContext; + @Nullable + private final IInlineContentProvider mInlineContentProvider; + @NonNull + private final Executor mCallbackExecutor; + + /** + * Callback from the client (IME) that will receive the inflated suggestion view. It'll + * only be called once when the view SurfacePackage is first sent back to the client. Any + * updates to the view due to attach to window and detach from window events will be + * handled under the hood, transparent from the client. + */ + @NonNull + private final Consumer<InlineContentView> mCallback; + + /** + * Indicates whether the first content has been received or not. + */ + private boolean mFirstContentReceived = false; + + /** + * The client (IME) side view which internally wraps a remote view. It'll be set when + * {@link #onContent(SurfaceControlViewHost.SurfacePackage, int, int)} is called, which + * should only happen once in the lifecycle of this inline suggestion instance. + */ + @Nullable + private InlineContentView mView; + + /** + * The SurfacePackage pointing to the remote view. It's cached here to be sent to the next + * available consumer. + */ + @Nullable + private SurfaceControlViewHost.SurfacePackage mSurfacePackage; + + /** + * The callback (from the {@link InlineContentView}) which consumes the surface package. + * It's cached here to be called when the SurfacePackage is returned from the remote + * view owning process. + */ + @Nullable + private Consumer<SurfaceControlViewHost.SurfacePackage> mSurfacePackageConsumer; InlineContentCallbackImpl(@NonNull Context context, + @Nullable IInlineContentProvider inlineContentProvider, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull Consumer<InlineContentView> callback) { mContext = context; + mInlineContentProvider = inlineContentProvider; mCallbackExecutor = callbackExecutor; mCallback = callback; } @@ -219,28 +280,110 @@ public final class InlineSuggestion implements Parcelable { @BinderThread public void onContent(SurfaceControlViewHost.SurfacePackage content, int width, int height) { - if (content == null) { + mMainHandler.post(() -> handleOnContent(content, width, height)); + } + + @MainThread + private void handleOnContent(SurfaceControlViewHost.SurfacePackage content, int width, + int height) { + if (!mFirstContentReceived) { + handleOnFirstContentReceived(content, width, height); + mFirstContentReceived = true; + } else { + handleOnSurfacePackage(content); + } + } + + /** + * Called when the view content is returned for the first time. + */ + @MainThread + private void handleOnFirstContentReceived(SurfaceControlViewHost.SurfacePackage content, + int width, int height) { + mSurfacePackage = content; + if (mSurfacePackage == null) { mCallbackExecutor.execute(() -> mCallback.accept(/* view */null)); } else { mView = new InlineContentView(mContext); mView.setLayoutParams(new ViewGroup.LayoutParams(width, height)); - mView.setChildSurfacePackage(content); + mView.setChildSurfacePackageUpdater(getSurfacePackageUpdater()); mCallbackExecutor.execute(() -> mCallback.accept(mView)); } } + /** + * Called when any subsequent SurfacePackage is returned from the remote view owning + * process. + */ + @MainThread + private void handleOnSurfacePackage(SurfaceControlViewHost.SurfacePackage surfacePackage) { + mSurfacePackage = surfacePackage; + if (mSurfacePackage != null && mSurfacePackageConsumer != null) { + mSurfacePackageConsumer.accept(mSurfacePackage); + mSurfacePackageConsumer = null; + } + } + + @MainThread + private void handleOnSurfacePackageReleased() { + mSurfacePackage = null; + try { + mInlineContentProvider.onSurfacePackageReleased(); + } catch (RemoteException e) { + Slog.w(TAG, "Error calling onSurfacePackageReleased(): " + e); + } + } + + @MainThread + private void handleGetSurfacePackage( + Consumer<SurfaceControlViewHost.SurfacePackage> consumer) { + if (mSurfacePackage != null) { + consumer.accept(mSurfacePackage); + } else { + mSurfacePackageConsumer = consumer; + try { + mInlineContentProvider.requestSurfacePackage(); + } catch (RemoteException e) { + Slog.w(TAG, "Error calling getSurfacePackage(): " + e); + consumer.accept(null); + mSurfacePackageConsumer = null; + } + } + } + + private InlineContentView.SurfacePackageUpdater getSurfacePackageUpdater() { + return new InlineContentView.SurfacePackageUpdater() { + @Override + public void onSurfacePackageReleased() { + mMainHandler.post( + () -> InlineContentCallbackImpl.this.handleOnSurfacePackageReleased()); + } + + @Override + public void getSurfacePackage( + Consumer<SurfaceControlViewHost.SurfacePackage> consumer) { + mMainHandler.post( + () -> InlineContentCallbackImpl.this.handleGetSurfacePackage(consumer)); + } + }; + } + @BinderThread public void onClick() { - if (mView != null && mView.hasOnClickListeners()) { - mView.callOnClick(); - } + mMainHandler.post(() -> { + if (mView != null && mView.hasOnClickListeners()) { + mView.callOnClick(); + } + }); } @BinderThread public void onLongClick() { - if (mView != null && mView.hasOnLongClickListeners()) { - mView.performLongClick(); - } + mMainHandler.post(() -> { + if (mView != null && mView.hasOnLongClickListeners()) { + mView.performLongClick(); + } + }); } } @@ -262,6 +405,7 @@ public final class InlineSuggestion implements Parcelable { + // Code below generated by codegen v1.0.15. // // DO NOT MODIFY! @@ -302,6 +446,14 @@ public final class InlineSuggestion implements Parcelable { } /** + * @hide + */ + @DataClass.Generated.Member + public @Nullable IInlineContentProvider getContentProvider() { + return mContentProvider; + } + + /** * Used to keep a strong reference to the callback so it doesn't get garbage collected. * * @hide @@ -421,7 +573,7 @@ public final class InlineSuggestion implements Parcelable { }; @DataClass.Generated( - time = 1587771173367L, + time = 1588308946517L, codegenVersion = "1.0.15", sourceFile = "frameworks/base/core/java/android/view/inputmethod/InlineSuggestion.java", inputSignatures = "private static final java.lang.String TAG\nprivate final @android.annotation.NonNull android.view.inputmethod.InlineSuggestionInfo mInfo\nprivate final @android.annotation.Nullable com.android.internal.view.inline.IInlineContentProvider mContentProvider\nprivate @com.android.internal.util.DataClass.ParcelWith(android.view.inputmethod.InlineSuggestion.InlineContentCallbackImplParceling.class) @android.annotation.Nullable android.view.inputmethod.InlineSuggestion.InlineContentCallbackImpl mInlineContentCallback\npublic static @android.annotation.TestApi @android.annotation.NonNull android.view.inputmethod.InlineSuggestion newInlineSuggestion(android.view.inputmethod.InlineSuggestionInfo)\npublic void inflate(android.content.Context,android.util.Size,java.util.concurrent.Executor,java.util.function.Consumer<android.widget.inline.InlineContentView>)\nprivate static boolean isValid(int,int,int)\nprivate synchronized android.view.inputmethod.InlineSuggestion.InlineContentCallbackImpl getInlineContentCallback(android.content.Context,java.util.concurrent.Executor,java.util.function.Consumer<android.widget.inline.InlineContentView>)\nclass InlineSuggestion extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true, genHiddenConstDefs=true, genHiddenConstructor=true)") diff --git a/core/java/android/widget/inline/InlineContentView.java b/core/java/android/widget/inline/InlineContentView.java index 4f2af63626cf..8657e828a3f6 100644 --- a/core/java/android/widget/inline/InlineContentView.java +++ b/core/java/android/widget/inline/InlineContentView.java @@ -21,40 +21,45 @@ import android.annotation.Nullable; import android.content.Context; import android.graphics.PixelFormat; import android.util.AttributeSet; +import android.util.Log; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.ViewGroup; +import java.util.function.Consumer; + /** - * This class represents a view that holds opaque content from another app that - * you can inline in your UI. + * This class represents a view that holds opaque content from another app that you can inline in + * your UI. * * <p>Since the content presented by this view is from another security domain,it is - * shown on a remote surface preventing the host application from accessing that content. - * Also the host application cannot interact with the inlined content by injecting touch - * events or clicking programmatically. + * shown on a remote surface preventing the host application from accessing that content. Also the + * host application cannot interact with the inlined content by injecting touch events or clicking + * programmatically. * * <p>This view can be overlaid by other windows, i.e. redressed, but if this is the case - * the inined UI would not be interactive. Sometimes this is desirable, e.g. animating - * transitions. + * the inlined UI would not be interactive. Sometimes this is desirable, e.g. animating transitions. * * <p>By default the surface backing this view is shown on top of the hosting window such - * that the inlined content is interactive. However, you can temporarily move the surface - * under the hosting window which could be useful in some cases, e.g. animating transitions. - * At this point the inlined content will not be interactive and the touch events would - * be delivered to your app. - * <p> - * Instances of this class are created by the platform and can be programmatically attached - * to your UI. Once you attach and detach this view it can not longer be reused and you - * should obtain a new view from the platform via the dedicated APIs. + * that the inlined content is interactive. However, you can temporarily move the surface under the + * hosting window which could be useful in some cases, e.g. animating transitions. At this point the + * inlined content will not be interactive and the touch events would be delivered to your app. + * + * <p> Instances of this class are created by the platform and can be programmatically attached to + * your UI. Once the view is attached to the window, you may detach and reattach it to the window. + * It should work seamlessly from the hosting process's point of view. */ public class InlineContentView extends ViewGroup { + private static final String TAG = "InlineContentView"; + + private static final boolean DEBUG = false; + /** - * Callback for observing the lifecycle of the surface control - * that manipulates the backing secure embedded UI surface. + * Callback for observing the lifecycle of the surface control that manipulates the backing + * secure embedded UI surface. */ public interface SurfaceControlCallback { /** @@ -72,15 +77,41 @@ public class InlineContentView extends ViewGroup { void onDestroyed(@NonNull SurfaceControl surfaceControl); } - private final @NonNull SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() { + /** + * Callback for sending an updated surface package in case the previous one is released + * from the detached from window event, and for getting notified of such event. + * + * This is expected to be provided to the {@link InlineContentView} so it can get updates + * from and send updates to the remote content (i.e. surface package) provider. + * + * @hide + */ + public interface SurfacePackageUpdater { + + /** + * Called when the previous surface package is released due to view being detached + * from the window. + */ + void onSurfacePackageReleased(); + + /** + * Called to request an updated surface package. + * + * @param consumer consumes the updated surface package. + */ + void getSurfacePackage(Consumer<SurfaceControlViewHost.SurfacePackage> consumer); + } + + @NonNull + private final SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() { @Override public void surfaceCreated(@NonNull SurfaceHolder holder) { mSurfaceControlCallback.onCreated(mSurfaceView.getSurfaceControl()); } @Override - public void surfaceChanged(@NonNull SurfaceHolder holder, - int format, int width, int height) { + public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, + int height) { /* do nothing */ } @@ -90,13 +121,17 @@ public class InlineContentView extends ViewGroup { } }; - private final @NonNull SurfaceView mSurfaceView; + @NonNull + private final SurfaceView mSurfaceView; + + @Nullable + private SurfaceControlCallback mSurfaceControlCallback; - private @Nullable SurfaceControlCallback mSurfaceControlCallback; + @Nullable + private SurfacePackageUpdater mSurfacePackageUpdater; /** * @inheritDoc - * * @hide */ public InlineContentView(@NonNull Context context) { @@ -105,7 +140,6 @@ public class InlineContentView extends ViewGroup { /** * @inheritDoc - * * @hide */ public InlineContentView(@NonNull Context context, @Nullable AttributeSet attrs) { @@ -114,7 +148,6 @@ public class InlineContentView extends ViewGroup { /** * @inheritDoc - * * @hide */ public InlineContentView(@NonNull Context context, @Nullable AttributeSet attrs, @@ -123,20 +156,18 @@ public class InlineContentView extends ViewGroup { } /** - * Gets the surface control. If the surface is not created this method - * returns {@code null}. + * Gets the surface control. If the surface is not created this method returns {@code null}. * * @return The surface control. - * * @see #setSurfaceControlCallback(SurfaceControlCallback) */ - public @Nullable SurfaceControl getSurfaceControl() { + @Nullable + public SurfaceControl getSurfaceControl() { return mSurfaceView.getSurfaceControl(); } /** * @inheritDoc - * * @hide */ public InlineContentView(@NonNull Context context, @Nullable AttributeSet attrs, @@ -149,14 +180,35 @@ public class InlineContentView extends ViewGroup { } /** - * Sets the embedded UI. - * @param surfacePackage The embedded UI. + * Sets the embedded UI provider. * * @hide */ - public void setChildSurfacePackage( - @Nullable SurfaceControlViewHost.SurfacePackage surfacePackage) { - mSurfaceView.setChildSurfacePackage(surfacePackage); + public void setChildSurfacePackageUpdater( + @Nullable SurfacePackageUpdater surfacePackageUpdater) { + mSurfacePackageUpdater = surfacePackageUpdater; + } + + @Override + protected void onAttachedToWindow() { + if (DEBUG) Log.v(TAG, "onAttachedToWindow"); + super.onAttachedToWindow(); + if (mSurfacePackageUpdater != null) { + mSurfacePackageUpdater.getSurfacePackage( + sp -> { + if (DEBUG) Log.v(TAG, "Received new SurfacePackage"); + mSurfaceView.setChildSurfacePackage(sp); + }); + } + } + + @Override + protected void onDetachedFromWindow() { + if (DEBUG) Log.v(TAG, "onDetachedFromWindow"); + super.onDetachedFromWindow(); + if (mSurfacePackageUpdater != null) { + mSurfacePackageUpdater.onSurfacePackageReleased(); + } } @Override @@ -165,8 +217,8 @@ public class InlineContentView extends ViewGroup { } /** - * Sets a callback to observe the lifecycle of the surface control for - * managing the backing surface. + * Sets a callback to observe the lifecycle of the surface control for managing the backing + * surface. * * @param callback The callback to set or {@code null} to clear. */ @@ -182,7 +234,6 @@ public class InlineContentView extends ViewGroup { /** * @return Whether the surface backing this view appears on top of its parent. - * * @see #setZOrderedOnTop(boolean) */ public boolean isZOrderedOnTop() { @@ -190,17 +241,15 @@ public class InlineContentView extends ViewGroup { } /** - * Controls whether the backing surface is placed on top of this view's window. - * Normally, it is placed on top of the window, to allow interaction - * with the inlined UI. Via this method, you can place the surface below the - * window. This means that all of the contents of the window this view is in - * will be visible on top of its surface. + * Controls whether the backing surface is placed on top of this view's window. Normally, it is + * placed on top of the window, to allow interaction with the inlined UI. Via this method, you + * can place the surface below the window. This means that all of the contents of the window + * this view is in will be visible on top of its surface. * * <p> The Z ordering can be changed dynamically if the backing surface is * created, otherwise the ordering would be applied at surface construction time. * * @param onTop Whether to show the surface on top of this view's window. - * * @see #isZOrderedOnTop() */ public boolean setZOrderedOnTop(boolean onTop) { diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 11ac3881ee38..3fc3f3e65d37 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -158,6 +158,7 @@ public class ChooserActivity extends ResolverActivity implements private static final String TAG = "ChooserActivity"; private AppPredictor mPersonalAppPredictor; private AppPredictor mWorkAppPredictor; + private boolean mShouldDisplayLandscape; @UnsupportedAppUsage public ChooserActivity() { @@ -716,6 +717,8 @@ public class ChooserActivity extends ResolverActivity implements mCallerChooserTargets = targets; } + mShouldDisplayLandscape = shouldDisplayLandscape( + getResources().getConfiguration().orientation); setRetainInOnStop(intent.getBooleanExtra(EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false)); super.onCreate(savedInstanceState, target, title, defaultTitleRes, initialIntents, null, false); @@ -1073,6 +1076,7 @@ public class ChooserActivity extends ResolverActivity implements public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); + mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation); adjustPreviewWidth(newConfig.orientation, null); updateStickyContentPreview(); } @@ -1086,7 +1090,7 @@ public class ChooserActivity extends ResolverActivity implements private void adjustPreviewWidth(int orientation, View parent) { int width = -1; - if (shouldDisplayLandscape(orientation)) { + if (mShouldDisplayLandscape) { width = getResources().getDimensionPixelSize(R.dimen.chooser_preview_width); } @@ -2940,6 +2944,19 @@ public class ChooserActivity extends ResolverActivity implements .setSubtype(previewType)); } + class ViewHolderBase extends RecyclerView.ViewHolder { + private int mViewType; + + ViewHolderBase(View itemView, int viewType) { + super(itemView); + this.mViewType = viewType; + } + + int getViewType() { + return mViewType; + } + } + /** * Used to bind types of individual item including * {@link ChooserGridAdapter#VIEW_TYPE_NORMAL}, @@ -2947,12 +2964,12 @@ public class ChooserActivity extends ResolverActivity implements * {@link ChooserGridAdapter#VIEW_TYPE_PROFILE}, * and {@link ChooserGridAdapter#VIEW_TYPE_AZ_LABEL}. */ - final class ItemViewHolder extends RecyclerView.ViewHolder { + final class ItemViewHolder extends ViewHolderBase { ResolverListAdapter.ViewHolder mWrappedViewHolder; int mListPosition = ChooserListAdapter.NO_POSITION; - ItemViewHolder(View itemView, boolean isClickable) { - super(itemView); + ItemViewHolder(View itemView, boolean isClickable, int viewType) { + super(itemView, viewType); mWrappedViewHolder = new ResolverListAdapter.ViewHolder(itemView); if (isClickable) { itemView.setOnClickListener(v -> startSelected(mListPosition, @@ -2970,9 +2987,9 @@ public class ChooserActivity extends ResolverActivity implements /** * Add a footer to the list, to support scrolling behavior below the navbar. */ - final class FooterViewHolder extends RecyclerView.ViewHolder { - FooterViewHolder(View itemView) { - super(itemView); + final class FooterViewHolder extends ViewHolderBase { + FooterViewHolder(View itemView, int viewType) { + super(itemView, viewType); } } @@ -3083,7 +3100,7 @@ public class ChooserActivity extends ResolverActivity implements int getMaxTargetsPerRow() { int maxTargets = MAX_TARGETS_PER_ROW_PORTRAIT; - if (shouldDisplayLandscape(getResources().getConfiguration().orientation)) { + if (mShouldDisplayLandscape) { maxTargets = MAX_TARGETS_PER_ROW_LANDSCAPE; } return maxTargets; @@ -3191,13 +3208,14 @@ public class ChooserActivity extends ResolverActivity implements public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { case VIEW_TYPE_CONTENT_PREVIEW: - return new ItemViewHolder(createContentPreviewView(parent), false); + return new ItemViewHolder(createContentPreviewView(parent), false, viewType); case VIEW_TYPE_PROFILE: - return new ItemViewHolder(createProfileView(parent), false); + return new ItemViewHolder(createProfileView(parent), false, viewType); case VIEW_TYPE_AZ_LABEL: - return new ItemViewHolder(createAzLabelView(parent), false); + return new ItemViewHolder(createAzLabelView(parent), false, viewType); case VIEW_TYPE_NORMAL: - return new ItemViewHolder(mChooserListAdapter.createView(parent), true); + return new ItemViewHolder( + mChooserListAdapter.createView(parent), true, viewType); case VIEW_TYPE_DIRECT_SHARE: case VIEW_TYPE_CALLER_AND_RANK: return createItemGroupViewHolder(viewType, parent); @@ -3205,7 +3223,7 @@ public class ChooserActivity extends ResolverActivity implements Space sp = new Space(parent.getContext()); sp.setLayoutParams(new RecyclerView.LayoutParams( LayoutParams.MATCH_PARENT, mFooterHeight)); - return new FooterViewHolder(sp); + return new FooterViewHolder(sp, viewType); default: // Since we catch all possible viewTypes above, no chance this is being called. return null; @@ -3214,7 +3232,7 @@ public class ChooserActivity extends ResolverActivity implements @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { - int viewType = getItemViewType(position); + int viewType = ((ViewHolderBase) holder).getViewType(); switch (viewType) { case VIEW_TYPE_DIRECT_SHARE: case VIEW_TYPE_CALLER_AND_RANK: @@ -3325,7 +3343,6 @@ public class ChooserActivity extends ResolverActivity implements } viewGroup.setTag(holder); - return holder; } @@ -3352,14 +3369,15 @@ public class ChooserActivity extends ResolverActivity implements parentGroup.addView(row2); mDirectShareViewHolder = new DirectShareViewHolder(parentGroup, - Lists.newArrayList(row1, row2), getMaxTargetsPerRow()); + Lists.newArrayList(row1, row2), getMaxTargetsPerRow(), viewType); loadViewsIntoGroup(mDirectShareViewHolder); return mDirectShareViewHolder; } else { ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row, parent, false); - ItemGroupViewHolder holder = new SingleRowViewHolder(row, getMaxTargetsPerRow()); + ItemGroupViewHolder holder = + new SingleRowViewHolder(row, getMaxTargetsPerRow(), viewType); loadViewsIntoGroup(holder); return holder; @@ -3521,14 +3539,14 @@ public class ChooserActivity extends ResolverActivity implements * {@link ChooserGridAdapter#VIEW_TYPE_DIRECT_SHARE}, * and {@link ChooserGridAdapter#VIEW_TYPE_CALLER_AND_RANK}. */ - abstract class ItemGroupViewHolder extends RecyclerView.ViewHolder { + abstract class ItemGroupViewHolder extends ViewHolderBase { protected int mMeasuredRowHeight; private int[] mItemIndices; protected final View[] mCells; private final int mColumnCount; - ItemGroupViewHolder(int cellCount, View itemView) { - super(itemView); + ItemGroupViewHolder(int cellCount, View itemView, int viewType) { + super(itemView, viewType); this.mCells = new View[cellCount]; this.mItemIndices = new int[cellCount]; this.mColumnCount = cellCount; @@ -3574,8 +3592,8 @@ public class ChooserActivity extends ResolverActivity implements class SingleRowViewHolder extends ItemGroupViewHolder { private final ViewGroup mRow; - SingleRowViewHolder(ViewGroup row, int cellCount) { - super(cellCount, row); + SingleRowViewHolder(ViewGroup row, int cellCount, int viewType) { + super(cellCount, row, viewType); this.mRow = row; } @@ -3617,8 +3635,9 @@ public class ChooserActivity extends ResolverActivity implements private final boolean[] mCellVisibility; - DirectShareViewHolder(ViewGroup parent, List<ViewGroup> rows, int cellCountPerRow) { - super(rows.size() * cellCountPerRow, parent); + DirectShareViewHolder(ViewGroup parent, List<ViewGroup> rows, int cellCountPerRow, + int viewType) { + super(rows.size() * cellCountPerRow, parent, viewType); this.mParent = parent; this.mRows = rows; diff --git a/core/java/com/android/internal/app/ChooserGridLayoutManager.java b/core/java/com/android/internal/app/ChooserGridLayoutManager.java new file mode 100644 index 000000000000..317a987cf359 --- /dev/null +++ b/core/java/com/android/internal/app/ChooserGridLayoutManager.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import android.content.Context; +import android.util.AttributeSet; + +import com.android.internal.widget.GridLayoutManager; +import com.android.internal.widget.RecyclerView; + +/** + * For a11y and per {@link RecyclerView#onInitializeAccessibilityNodeInfo}, override + * methods to ensure proper row counts. + */ +public class ChooserGridLayoutManager extends GridLayoutManager { + + /** + * Constructor used when layout manager is set in XML by RecyclerView attribute + * "layoutManager". If spanCount is not specified in the XML, it defaults to a + * single column. + * + */ + public ChooserGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + /** + * Creates a vertical GridLayoutManager + * + * @param context Current context, will be used to access resources. + * @param spanCount The number of columns in the grid + */ + public ChooserGridLayoutManager(Context context, int spanCount) { + super(context, spanCount); + } + + /** + * @param context Current context, will be used to access resources. + * @param spanCount The number of columns or rows in the grid + * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link + * #VERTICAL}. + * @param reverseLayout When set to true, layouts from end to start. + */ + public ChooserGridLayoutManager(Context context, int spanCount, int orientation, + boolean reverseLayout) { + super(context, spanCount, orientation, reverseLayout); + } + + @Override + public int getRowCountForAccessibility(RecyclerView.Recycler recycler, + RecyclerView.State state) { + // Do not count the footer view in the official count + return super.getRowCountForAccessibility(recycler, state) - 1; + } +} diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 2f62f8e7a0c9..83dabe8d0525 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -182,6 +182,8 @@ public class ResolverActivity extends Activity implements private BroadcastReceiver mWorkProfileStateReceiver; private UserHandle mHeaderCreatorUser; + private UserHandle mWorkProfileUserHandle; + /** * Get the string resource to be used as a label for the link to the resolver activity for an * action. @@ -363,6 +365,7 @@ public class ResolverActivity extends Activity implements // a more complicated UI that the current voice interaction flow is not able // to handle. boolean filterLastUsed = mSupportsAlwaysUseOption && !isVoiceInteraction(); + mWorkProfileUserHandle = fetchWorkProfileUserProfile(); mMultiProfilePagerAdapter = createMultiProfilePagerAdapter(initialIntents, rList, filterLastUsed); if (configureContentView()) { return; @@ -527,13 +530,18 @@ public class ResolverActivity extends Activity implements return UserHandle.of(ActivityManager.getCurrentUser()); } protected @Nullable UserHandle getWorkProfileUserHandle() { + return mWorkProfileUserHandle; + } + + protected @Nullable UserHandle fetchWorkProfileUserProfile() { + mWorkProfileUserHandle = null; UserManager userManager = getSystemService(UserManager.class); for (final UserInfo userInfo : userManager.getProfiles(ActivityManager.getCurrentUser())) { if (userInfo.isManagedProfile()) { - return userInfo.getUserHandle(); + mWorkProfileUserHandle = userInfo.getUserHandle(); } } - return null; + return mWorkProfileUserHandle; } private boolean hasWorkProfile() { diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java index 2fd938f45291..24bf98b6502c 100644 --- a/core/java/com/android/internal/app/ResolverListAdapter.java +++ b/core/java/com/android/internal/app/ResolverListAdapter.java @@ -54,6 +54,7 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; import com.android.internal.app.chooser.DisplayResolveInfo; +import com.android.internal.app.chooser.SelectableTargetInfo; import com.android.internal.app.chooser.TargetInfo; import java.util.ArrayList; @@ -549,6 +550,15 @@ public class ResolverListAdapter extends BaseAdapter { getLoadLabelTask((DisplayResolveInfo) info, holder).execute(); } else { holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo()); + 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); + } } if (info.isSuspended()) { @@ -697,6 +707,12 @@ public class ResolverListAdapter extends BaseAdapter { text2.setVisibility(View.VISIBLE); text2.setText(subLabel); } + + itemView.setContentDescription(null); + } + + public void updateContentDescription(String description) { + itemView.setContentDescription(description); } } diff --git a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java index 246a07d3d0fe..900e18d468bb 100644 --- a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java +++ b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java @@ -44,7 +44,6 @@ import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGett import com.android.internal.app.SimpleIconFactory; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; /** @@ -136,6 +135,10 @@ public final class SelectableTargetInfo implements ChooserTargetInfo { return mIsSuspended; } + public DisplayResolveInfo getDisplayResolveInfo() { + return mSourceInfo; + } + private Drawable getChooserTargetIconDrawable(ChooserTarget target, @Nullable ShortcutInfo shortcutInfo) { Drawable directShareIcon = null; diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 38f5f3279c8e..8c5fdf5822e8 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -137,7 +137,7 @@ oneway interface IStatusBar // Used to show the authentication dialog (Biometrics, Device Credential) void showAuthenticationDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver, int biometricModality, boolean requireConfirmation, int userId, String opPackageName, - long operationId); + long operationId, int sysUiSessionId); // Used to notify the authentication dialog that a biometric has been authenticated void onBiometricAuthenticated(); // Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 24fe0638b091..c32082418bc5 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -106,7 +106,7 @@ interface IStatusBarService // Used to show the authentication dialog (Biometrics, Device Credential) void showAuthenticationDialog(in Bundle bundle, IBiometricServiceReceiverInternal receiver, int biometricModality, boolean requireConfirmation, int userId, String opPackageName, - long operationId); + long operationId, int sysUiSessionId); // Used to notify the authentication dialog that a biometric has been authenticated void onBiometricAuthenticated(); // Used to set a temporary message, e.g. fingerprint not recognized, finger moved too fast, etc diff --git a/core/java/com/android/internal/view/inline/IInlineContentProvider.aidl b/core/java/com/android/internal/view/inline/IInlineContentProvider.aidl index 08a349c21c8b..78df3eb660a5 100644 --- a/core/java/com/android/internal/view/inline/IInlineContentProvider.aidl +++ b/core/java/com/android/internal/view/inline/IInlineContentProvider.aidl @@ -24,4 +24,6 @@ import com.android.internal.view.inline.IInlineContentCallback; */ oneway interface IInlineContentProvider { void provideContent(int width, int height, in IInlineContentCallback callback); + void requestSurfacePackage(); + void onSurfacePackageReleased(); } diff --git a/core/java/com/android/internal/widget/GridLayoutManager.java b/core/java/com/android/internal/widget/GridLayoutManager.java index e0502f129f7f..09e6a991b1ac 100644 --- a/core/java/com/android/internal/widget/GridLayoutManager.java +++ b/core/java/com/android/internal/widget/GridLayoutManager.java @@ -153,13 +153,11 @@ public class GridLayoutManager extends LinearLayoutManager { if (mOrientation == HORIZONTAL) { info.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain( glp.getSpanIndex(), glp.getSpanSize(), - spanGroupIndex, 1, - mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false)); + spanGroupIndex, 1, false, false)); } else { // VERTICAL info.setCollectionItemInfo(AccessibilityNodeInfo.CollectionItemInfo.obtain( spanGroupIndex, 1, - glp.getSpanIndex(), glp.getSpanSize(), - mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false)); + glp.getSpanIndex(), glp.getSpanSize(), false, false)); } } diff --git a/core/java/com/android/internal/widget/ResolverDrawerLayout.java b/core/java/com/android/internal/widget/ResolverDrawerLayout.java index fb2ecf3a478f..3f708f84750c 100644 --- a/core/java/com/android/internal/widget/ResolverDrawerLayout.java +++ b/core/java/com/android/internal/widget/ResolverDrawerLayout.java @@ -825,18 +825,6 @@ public class ResolverDrawerLayout extends ViewGroup { return true; } break; - case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: - case R.id.accessibilityActionScrollUp: - if (mCollapseOffset < mCollapsibleHeight) { - smoothScrollTo(mCollapsibleHeight, 0); - return true; - } else if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight) - && isDismissable()) { - smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, 0); - mDismissOnScrollerFinished = true; - return true; - } - break; case AccessibilityNodeInfo.ACTION_COLLAPSE: if (mCollapseOffset < mCollapsibleHeight) { smoothScrollTo(mCollapsibleHeight, 0); @@ -886,7 +874,6 @@ public class ResolverDrawerLayout extends ViewGroup { } if ((mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight) && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) { - info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD); info.addAction(AccessibilityAction.ACTION_SCROLL_UP); info.setScrollable(true); } diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 3a5720fd8c4c..c5bc083dfabf 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -1552,8 +1552,8 @@ static void isolateJitProfile(JNIEnv* env, jobjectArray pkg_data_info_list, } } -static void BindMountStorageToLowerFs(const userid_t user_id, const char* dir_name, - const char* package, fail_fn_t fail_fn) { +static void BindMountStorageToLowerFs(const userid_t user_id, const uid_t uid, + const char* dir_name, const char* package, fail_fn_t fail_fn) { bool hasSdcardFs = IsFilesystemSupported("sdcardfs"); std::string source; @@ -1565,6 +1565,9 @@ static void BindMountStorageToLowerFs(const userid_t user_id, const char* dir_na } std::string target = StringPrintf("/storage/emulated/%d/%s/%s", user_id, dir_name, package); + // As the parent is mounted as tmpfs, we need to create the target dir here. + PrepareDirIfNotPresent(target, 0700, uid, uid, fail_fn); + if (access(source.c_str(), F_OK) != 0) { fail_fn(CREATE_ERROR("Error accessing %s: %s", source.c_str(), strerror(errno))); } @@ -1574,9 +1577,8 @@ static void BindMountStorageToLowerFs(const userid_t user_id, const char* dir_na BindMount(source, target, fail_fn); } -// Bind mount all obb & data directories that are visible to this app. -// If app data isolation is not enabled for this process, bind mount the whole obb -// and data directory instead. +// Mount tmpfs on Android/data and Android/obb, then bind mount all app visible package +// directories in data and obb directories. static void BindMountStorageDirs(JNIEnv* env, jobjectArray pkg_data_info_list, uid_t uid, const char* process_name, jstring managed_nice_name, fail_fn_t fail_fn) { @@ -1590,12 +1592,18 @@ static void BindMountStorageDirs(JNIEnv* env, jobjectArray pkg_data_info_list, fail_fn(CREATE_ERROR("Data package list cannot be empty")); } + // Create tmpfs on Android/obb and Android/data so these 2 dirs won't enter fuse anymore. + std::string androidObbDir = StringPrintf("/storage/emulated/%d/Android/obb", user_id); + MountAppDataTmpFs(androidObbDir, fail_fn); + std::string androidDataDir = StringPrintf("/storage/emulated/%d/Android/data", user_id); + MountAppDataTmpFs(androidDataDir, fail_fn); + // Bind mount each package obb directory for (int i = 0; i < size; i += 3) { jstring package_str = (jstring) (env->GetObjectArrayElement(pkg_data_info_list, i)); std::string packageName = extract_fn(package_str).value(); - BindMountStorageToLowerFs(user_id, "Android/obb", packageName.c_str(), fail_fn); - BindMountStorageToLowerFs(user_id, "Android/data", packageName.c_str(), fail_fn); + BindMountStorageToLowerFs(user_id, uid, "Android/obb", packageName.c_str(), fail_fn); + BindMountStorageToLowerFs(user_id, uid, "Android/data", packageName.c_str(), fail_fn); } } @@ -1648,9 +1656,10 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, uid, process_name, managed_nice_name, fail_fn); isolateJitProfile(env, pkg_data_info_list, uid, process_name, managed_nice_name, fail_fn); } - if (mount_external != MOUNT_EXTERNAL_INSTALLER && - mount_external != MOUNT_EXTERNAL_PASS_THROUGH && - mount_storage_dirs) { + // MOUNT_EXTERNAL_INSTALLER, MOUNT_EXTERNAL_PASS_THROUGH, MOUNT_EXTERNAL_ANDROID_WRITABLE apps + // will have mount_storage_dirs == false here (set by ProcessList.needsStorageDataIsolation()), + // and hence they won't bind mount storage dirs. + if (mount_storage_dirs) { BindMountStorageDirs(env, pkg_data_info_list, uid, process_name, managed_nice_name, fail_fn); } diff --git a/core/res/res/layout/chooser_list_per_profile.xml b/core/res/res/layout/chooser_list_per_profile.xml index 6b1b002267cb..86dc71cbbfb8 100644 --- a/core/res/res/layout/chooser_list_per_profile.xml +++ b/core/res/res/layout/chooser_list_per_profile.xml @@ -20,7 +20,7 @@ <com.android.internal.widget.RecyclerView android:layout_width="match_parent" android:layout_height="match_parent" - android:layoutManager="com.android.internal.widget.GridLayoutManager" + android:layoutManager="com.android.internal.app.ChooserGridLayoutManager" android:id="@+id/resolver_list" android:clipToPadding="false" android:background="?attr/colorBackgroundFloating" @@ -29,4 +29,4 @@ android:nestedScrollingEnabled="true" /> <include layout="@layout/resolver_empty_states" /> -</RelativeLayout>
\ No newline at end of file +</RelativeLayout> diff --git a/core/res/res/layout/resolver_list.xml b/core/res/res/layout/resolver_list.xml index 4d0837f495df..446ce3fbaf4b 100644 --- a/core/res/res/layout/resolver_list.xml +++ b/core/res/res/layout/resolver_list.xml @@ -83,6 +83,7 @@ android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" + android:accessibilityTraversalAfter="@id/title" android:background="?attr/colorBackgroundFloating"> <LinearLayout android:orientation="vertical" diff --git a/core/tests/coretests/src/android/content/pm/PackageManagerTests.java b/core/tests/coretests/src/android/content/pm/PackageManagerTests.java index 6720ed6b7bf9..04906788f4cb 100644 --- a/core/tests/coretests/src/android/content/pm/PackageManagerTests.java +++ b/core/tests/coretests/src/android/content/pm/PackageManagerTests.java @@ -2548,9 +2548,18 @@ public class PackageManagerTests extends AndroidTestCase { } else { installFromRawResource(apk2Name, apk2, 0, false, false, -1, PackageInfo.INSTALL_LOCATION_UNSPECIFIED); - int match = mContext.getPackageManager().checkSignatures(pkg1.getPackageName(), - pkg2.getPackageName()); - assertEquals(expMatchResult, match); + // TODO: All checkSignatures tests should return the same result regardless of + // querying by package name or uid; however if there are any edge cases where + // individual packages within a shareduid are compared with signatures that do not + // match the full lineage of the shareduid this method should be overloaded to + // accept the expected response for the uid query. + PackageManager pm = getPm(); + int matchByName = pm.checkSignatures(pkg1.getPackageName(), pkg2.getPackageName()); + int pkg1Uid = pm.getApplicationInfo(pkg1.getPackageName(), 0).uid; + int pkg2Uid = pm.getApplicationInfo(pkg2.getPackageName(), 0).uid; + int matchByUid = pm.checkSignatures(pkg1Uid, pkg2Uid); + assertEquals(expMatchResult, matchByName); + assertEquals(expMatchResult, matchByUid); } } finally { if (cleanUp) { diff --git a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java index 164c372768c0..bfcf52af80bf 100644 --- a/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java +++ b/core/tests/coretests/src/android/view/ImeInsetsSourceConsumerTest.java @@ -100,12 +100,12 @@ public class ImeInsetsSourceConsumerTest { // test if setVisibility can show IME mImeConsumer.onWindowFocusGained(); mImeConsumer.applyImeVisibility(true); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertTrue(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); // test if setVisibility can hide IME mImeConsumer.applyImeVisibility(false); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); }); } diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index cc85332590ba..d4c256972b28 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -245,14 +245,14 @@ public class InsetsControllerTest { mController.applyImeVisibility(true /* setVisible */); mController.show(Type.all()); // quickly jump to final state by cancelling it. - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertTrue(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); mController.applyImeVisibility(false /* setVisible */); mController.hide(Type.all()); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); @@ -268,10 +268,10 @@ public class InsetsControllerTest { InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { mController.getSourceConsumer(ITYPE_IME).onWindowFocusGained(); mController.applyImeVisibility(true); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertTrue(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); mController.applyImeVisibility(false); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); mController.getSourceConsumer(ITYPE_IME).onWindowFocusLost(); }); @@ -291,7 +291,7 @@ public class InsetsControllerTest { mController.hide(types); assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_NAVIGATION_BAR)); assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_STATUS_BAR)); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertEquals(ANIMATION_TYPE_NONE, mController.getAnimationType(ITYPE_NAVIGATION_BAR)); assertEquals(ANIMATION_TYPE_NONE, mController.getAnimationType(ITYPE_STATUS_BAR)); assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); @@ -302,7 +302,7 @@ public class InsetsControllerTest { mController.show(types); assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ITYPE_NAVIGATION_BAR)); assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ITYPE_STATUS_BAR)); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); @@ -321,21 +321,21 @@ public class InsetsControllerTest { int types = Type.navigationBars() | Type.systemBars(); // test show select types. mController.show(types); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); // test hide all mController.hide(Type.all()); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); // test single show mController.show(Type.navigationBars()); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); @@ -363,7 +363,7 @@ public class InsetsControllerTest { mController.hide(Type.systemBars()); assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_NAVIGATION_BAR)); assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_STATUS_BAR)); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); @@ -372,7 +372,7 @@ public class InsetsControllerTest { mController.show(Type.systemBars()); assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ITYPE_NAVIGATION_BAR)); assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ITYPE_STATUS_BAR)); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertTrue(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); @@ -383,7 +383,7 @@ public class InsetsControllerTest { mController.hide(Type.navigationBars()); assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_NAVIGATION_BAR)); assertEquals(ANIMATION_TYPE_NONE, mController.getAnimationType(ITYPE_STATUS_BAR)); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); @@ -391,7 +391,7 @@ public class InsetsControllerTest { mController.hide(Type.systemBars()); assertEquals(ANIMATION_TYPE_NONE, mController.getAnimationType(ITYPE_NAVIGATION_BAR)); assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_STATUS_BAR)); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); @@ -411,13 +411,13 @@ public class InsetsControllerTest { // show two at a time and hide one by one. mController.show(types); mController.hide(Type.navigationBars()); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertTrue(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); mController.hide(Type.systemBars()); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(navBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(statusBar.getType()).isRequestedVisible()); assertFalse(mController.getSourceConsumer(ime.getType()).isRequestedVisible()); @@ -431,7 +431,7 @@ public class InsetsControllerTest { InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { mController.hide(Type.statusBars()); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(ITYPE_STATUS_BAR).isRequestedVisible()); assertFalse(mController.getState().getSource(ITYPE_STATUS_BAR).isVisible()); @@ -446,7 +446,7 @@ public class InsetsControllerTest { // Gaining control mController.onControlsChanged(createSingletonControl(ITYPE_STATUS_BAR)); assertEquals(ANIMATION_TYPE_HIDE, mController.getAnimationType(ITYPE_STATUS_BAR)); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertFalse(mController.getSourceConsumer(ITYPE_STATUS_BAR).isRequestedVisible()); assertFalse(mController.getState().getSource(ITYPE_STATUS_BAR).isVisible()); }); @@ -468,7 +468,7 @@ public class InsetsControllerTest { mController.onControlsChanged(createSingletonControl(ITYPE_IME)); assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ITYPE_IME)); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertTrue(mController.getSourceConsumer(ITYPE_IME).isRequestedVisible()); assertTrue(mController.getState().getSource(ITYPE_IME).isVisible()); }); @@ -489,7 +489,7 @@ public class InsetsControllerTest { mController.show(ime(), true /* fromIme */); assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ITYPE_IME)); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertTrue(mController.getSourceConsumer(ITYPE_IME).isRequestedVisible()); assertTrue(mController.getState().getSource(ITYPE_IME).isVisible()); }); @@ -658,7 +658,7 @@ public class InsetsControllerTest { mController.getState().getSource(ITYPE_IME).getFrame()); assertNotEquals(new Rect(4, 5, 6, 7), mController.getState().getSource(ITYPE_IME).getVisibleFrame()); - mController.cancelExistingAnimation(); + mController.cancelExistingAnimations(); assertEquals(new Rect(0, 1, 2, 3), mController.getState().getSource(ITYPE_IME).getFrame()); assertEquals(new Rect(4, 5, 6, 7), diff --git a/media/java/android/media/AudioMetadata.java b/media/java/android/media/AudioMetadata.java index c91ff0d099cf..ff9fd4187272 100644 --- a/media/java/android/media/AudioMetadata.java +++ b/media/java/android/media/AudioMetadata.java @@ -166,10 +166,25 @@ public final class AudioMetadata { * * A Boolean value which is true if Atmos is present in an E-AC3 stream. */ + + // Since Boolean isn't handled by Parceling, we translate + // internally to KEY_HAS_ATMOS when sending through JNI. + // Consider deprecating this key for KEY_HAS_ATMOS in the future. + // @NonNull public static final Key<Boolean> KEY_ATMOS_PRESENT = createKey("atmos-present", Boolean.class); /** + * A key representing the presence of Atmos in an E-AC3 stream. + * + * An Integer value which is nonzero if Atmos is present in an E-AC3 stream. + * The integer representation is used for communication to the native side. + * @hide + */ + @NonNull public static final Key<Integer> KEY_HAS_ATMOS = + createKey("has-atmos", Integer.class); + + /** * A key representing the audio encoding used for the stream. * This is the same encoding used in {@link AudioFormat#getEncoding()}. * @@ -731,6 +746,15 @@ public final class AudioMetadata { Log.e(TAG, "Failed to unpack value for map"); return null; } + + // Special handling of KEY_ATMOS_PRESENT. + if (key.equals(Format.KEY_HAS_ATMOS.getName()) + && value.first == Format.KEY_HAS_ATMOS.getValueClass()) { + ret.set(Format.KEY_ATMOS_PRESENT, + (Boolean) ((int) value.second != 0)); // Translate Integer to Boolean + continue; // Should we store both keys in the java table? + } + ret.set(createKey(key, value.first), value.first.cast(value.second)); } return ret; @@ -746,11 +770,19 @@ public final class AudioMetadata { return false; } for (Key<?> key : obj.keySet()) { + Object value = obj.get(key); + + // Special handling of KEY_ATMOS_PRESENT. + if (key == Format.KEY_ATMOS_PRESENT) { + key = Format.KEY_HAS_ATMOS; + value = (Integer) ((boolean) value ? 1 : 0); // Translate Boolean to Integer + } + if (!strDataPackage.pack(output, key.getName())) { Log.i(TAG, "Failed to pack key: " + key.getName()); return false; } - if (!OBJECT_PACKAGE.pack(output, new Pair<>(key.getValueClass(), obj.get(key)))) { + if (!OBJECT_PACKAGE.pack(output, new Pair<>(key.getValueClass(), value))) { Log.i(TAG, "Failed to pack value: " + obj.get(key)); return false; } diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java index 6c9013fe37c4..e3c7336905d1 100644 --- a/media/java/android/media/MediaRouter2Manager.java +++ b/media/java/android/media/MediaRouter2Manager.java @@ -170,8 +170,7 @@ public final class MediaRouter2Manager { public MediaController getMediaControllerForRoutingSession( @NonNull RoutingSessionInfo sessionInfo) { for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) { - String volumeControlId = controller.getPlaybackInfo().getVolumeControlId(); - if (TextUtils.equals(sessionInfo.getId(), volumeControlId)) { + if (areSessionsMatched(controller, sessionInfo)) { return controller; } } @@ -206,6 +205,37 @@ public final class MediaRouter2Manager { } /** + * Gets available routes for the given routing session. + * The returned routes can be passed to + * {@link #transfer(RoutingSessionInfo, MediaRoute2Info)} for transferring the routing session. + * + * @param sessionInfo the routing session that would be transferred + */ + @NonNull + public List<MediaRoute2Info> getAvailableRoutesForRoutingSession( + @NonNull RoutingSessionInfo sessionInfo) { + Objects.requireNonNull(sessionInfo, "sessionInfo must not be null"); + + List<MediaRoute2Info> routes = new ArrayList<>(); + + String packageName = sessionInfo.getClientPackageName(); + List<String> preferredFeatures = mPreferredFeaturesMap.get(packageName); + if (preferredFeatures == null) { + preferredFeatures = Collections.emptyList(); + } + synchronized (mRoutesLock) { + for (MediaRoute2Info route : mRoutes.values()) { + if (route.isSystemRoute() || route.hasAnyFeatures(preferredFeatures) + || sessionInfo.getSelectedRoutes().contains(route.getId()) + || sessionInfo.getTransferableRoutes().contains(route.getId())) { + routes.add(route); + } + } + } + return routes; + } + + /** * Gets the system routing session associated with no specific application. */ @NonNull @@ -219,6 +249,33 @@ public final class MediaRouter2Manager { } /** + * Gets the routing session of a media session. + * If the session is using {#link PlaybackInfo#PLAYBACK_TYPE_LOCAL local playback}, + * the system routing session is returned. + * If the session is using {#link PlaybackInfo#PLAYBACK_TYPE_REMOTE remote playback}, + * it returns the corresponding routing session or {@code null} if it's unavailable. + */ + @Nullable + public RoutingSessionInfo getRoutingSessionForMediaController(MediaController mediaController) { + MediaController.PlaybackInfo playbackInfo = mediaController.getPlaybackInfo(); + if (playbackInfo == null) { + return null; + } + if (playbackInfo.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) { + return new RoutingSessionInfo.Builder(getSystemRoutingSession()) + .setClientPackageName(mediaController.getPackageName()) + .build(); + } + for (RoutingSessionInfo sessionInfo : getActiveSessions()) { + if (!sessionInfo.isSystemSession() + && areSessionsMatched(mediaController, sessionInfo)) { + return sessionInfo; + } + } + return null; + } + + /** * Gets routing sessions of an application with the given package name. * The first element of the returned list is the system routing session. * @@ -762,6 +819,27 @@ public final class MediaRouter2Manager { } } + private boolean areSessionsMatched(MediaController mediaController, + RoutingSessionInfo sessionInfo) { + MediaController.PlaybackInfo playbackInfo = mediaController.getPlaybackInfo(); + if (playbackInfo == null) { + return false; + } + + String volumeControlId = playbackInfo.getVolumeControlId(); + if (volumeControlId == null) { + return false; + } + + if (TextUtils.equals(volumeControlId, sessionInfo.getId())) { + return true; + } + // Workaround for provider not being able to know the unique session ID. + return TextUtils.equals(volumeControlId, sessionInfo.getOriginalId()) + && TextUtils.equals(mediaController.getPackageName(), + sessionInfo.getOwnerPackageName()); + } + private List<MediaRoute2Info> getRoutesWithIds(List<String> routeIds) { synchronized (sLock) { return routeIds.stream().map(mRoutes::get) diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java index 608e29a7a6ca..edf1fc58ecf5 100644 --- a/media/java/android/media/RoutingSessionInfo.java +++ b/media/java/android/media/RoutingSessionInfo.java @@ -50,6 +50,7 @@ public final class RoutingSessionInfo implements Parcelable { final String mId; final CharSequence mName; + final String mOwnerPackageName; final String mClientPackageName; @Nullable final String mProviderId; @@ -71,6 +72,7 @@ public final class RoutingSessionInfo implements Parcelable { mId = builder.mId; mName = builder.mName; + mOwnerPackageName = builder.mOwnerPackageName; mClientPackageName = builder.mClientPackageName; mProviderId = builder.mProviderId; @@ -96,6 +98,7 @@ public final class RoutingSessionInfo implements Parcelable { mId = ensureString(src.readString()); mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(src); + mOwnerPackageName = src.readString(); mClientPackageName = ensureString(src.readString()); mProviderId = src.readString(); @@ -159,6 +162,15 @@ public final class RoutingSessionInfo implements Parcelable { } /** + * Gets the package name of the session owner. + * @hide + */ + @Nullable + public String getOwnerPackageName() { + return mOwnerPackageName; + } + + /** * Gets the client package name of the session */ @NonNull @@ -263,6 +275,7 @@ public final class RoutingSessionInfo implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeString(mId); dest.writeCharSequence(mName); + dest.writeString(mOwnerPackageName); dest.writeString(mClientPackageName); dest.writeString(mProviderId); dest.writeStringList(mSelectedRoutes); @@ -288,6 +301,7 @@ public final class RoutingSessionInfo implements Parcelable { RoutingSessionInfo other = (RoutingSessionInfo) obj; return Objects.equals(mId, other.mId) && Objects.equals(mName, other.mName) + && Objects.equals(mOwnerPackageName, other.mOwnerPackageName) && Objects.equals(mClientPackageName, other.mClientPackageName) && Objects.equals(mProviderId, other.mProviderId) && Objects.equals(mSelectedRoutes, other.mSelectedRoutes) @@ -301,7 +315,7 @@ public final class RoutingSessionInfo implements Parcelable { @Override public int hashCode() { - return Objects.hash(mId, mName, mClientPackageName, mProviderId, + return Objects.hash(mId, mName, mOwnerPackageName, mClientPackageName, mProviderId, mSelectedRoutes, mSelectableRoutes, mDeselectableRoutes, mTransferableRoutes, mVolumeMax, mVolumeHandling, mVolume); } @@ -356,6 +370,7 @@ public final class RoutingSessionInfo implements Parcelable { // TODO: Reorder these (important ones first) final String mId; CharSequence mName; + String mOwnerPackageName; String mClientPackageName; String mProviderId; final List<String> mSelectedRoutes; @@ -440,6 +455,17 @@ public final class RoutingSessionInfo implements Parcelable { } /** + * Sets the package name of the session owner. It is expected to be called by the system. + * + * @hide + */ + @NonNull + public Builder setOwnerPackageName(@Nullable String packageName) { + mOwnerPackageName = packageName; + return this; + } + + /** * Sets the client package name of the session. * * @hide diff --git a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarController.java b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarController.java index 9e194fb49d3a..288e5cf13c2e 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarController.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/navigationbar/CarNavigationBarController.java @@ -73,7 +73,7 @@ public class CarNavigationBarController { } /** - * Hides all navigation bars. + * Hides all system bars. */ public void hideBars() { if (mTopView != null) { @@ -85,7 +85,7 @@ public class CarNavigationBarController { } /** - * Shows all navigation bars. + * Shows all system bars. */ public void showBars() { if (mTopView != null) { diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java index 20fcca0d0220..aeb1d39599db 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java @@ -29,41 +29,40 @@ import android.widget.FrameLayout; import com.android.car.notification.R; import com.android.car.notification.headsup.CarHeadsUpNotificationContainer; import com.android.systemui.car.CarDeviceProvisionedController; +import com.android.systemui.car.window.OverlayViewGlobalStateController; import com.android.systemui.dagger.qualifiers.Main; import javax.inject.Inject; import javax.inject.Singleton; -import dagger.Lazy; - /** * A controller for SysUI's HUN display. */ @Singleton public class CarHeadsUpNotificationSystemContainer implements CarHeadsUpNotificationContainer { private final CarDeviceProvisionedController mCarDeviceProvisionedController; - private final Lazy<NotificationPanelViewController> mNotificationPanelViewControllerLazy; + private final OverlayViewGlobalStateController mOverlayViewGlobalStateController; private final ViewGroup mWindow; private final FrameLayout mHeadsUpContentFrame; - private final boolean mEnableHeadsUpNotificationWhenNotificationShadeOpen; - @Inject CarHeadsUpNotificationSystemContainer(Context context, @Main Resources resources, CarDeviceProvisionedController deviceProvisionedController, WindowManager windowManager, - Lazy<NotificationPanelViewController> notificationPanelViewControllerLazy) { + OverlayViewGlobalStateController overlayViewGlobalStateController) { mCarDeviceProvisionedController = deviceProvisionedController; - mNotificationPanelViewControllerLazy = notificationPanelViewControllerLazy; + mOverlayViewGlobalStateController = overlayViewGlobalStateController; boolean showOnBottom = resources.getBoolean(R.bool.config_showHeadsUpNotificationOnBottom); + // Use TYPE_STATUS_BAR_SUB_PANEL window type since we need to find a window that is above + // status bar but below navigation bar. WindowManager.LayoutParams lp = new WindowManager.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT, - WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG, + WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT); @@ -78,15 +77,11 @@ public class CarHeadsUpNotificationSystemContainer implements CarHeadsUpNotifica windowManager.addView(mWindow, lp); mWindow.setVisibility(View.INVISIBLE); mHeadsUpContentFrame = mWindow.findViewById(R.id.headsup_content); - - mEnableHeadsUpNotificationWhenNotificationShadeOpen = resources.getBoolean( - R.bool.config_enableHeadsUpNotificationWhenNotificationShadeOpen); } private void animateShow() { - if ((mEnableHeadsUpNotificationWhenNotificationShadeOpen - || !mNotificationPanelViewControllerLazy.get().isPanelExpanded()) - && mCarDeviceProvisionedController.isCurrentUserFullySetup()) { + if (mCarDeviceProvisionedController.isCurrentUserFullySetup() + && mOverlayViewGlobalStateController.shouldShowHUN()) { mWindow.setVisibility(View.VISIBLE); } } diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java index cb9539ad5b1d..1738091d14c9 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationPanelViewController.java @@ -73,6 +73,7 @@ public class NotificationPanelViewController extends OverlayPanelViewController private final CarNotificationListener mCarNotificationListener; private final NotificationClickHandlerFactory mNotificationClickHandlerFactory; private final StatusBarStateController mStatusBarStateController; + private final boolean mEnableHeadsUpNotificationWhenNotificationShadeOpen; private float mInitialBackgroundAlpha; private float mBackgroundAlphaDiff; @@ -144,6 +145,10 @@ public class NotificationPanelViewController extends OverlayPanelViewController + " percentage"); } mBackgroundAlphaDiff = finalBackgroundAlpha - mInitialBackgroundAlpha; + + mEnableHeadsUpNotificationWhenNotificationShadeOpen = mResources.getBoolean( + com.android.car.notification.R.bool + .config_enableHeadsUpNotificationWhenNotificationShadeOpen); } @Override @@ -151,6 +156,16 @@ public class NotificationPanelViewController extends OverlayPanelViewController reinflate(); } + @Override + protected boolean shouldShowNavigationBar() { + return true; + } + + @Override + protected boolean shouldShowHUN() { + return mEnableHeadsUpNotificationWhenNotificationShadeOpen; + } + /** Reinflates the view. */ public void reinflate() { ViewGroup container = (ViewGroup) getLayout(); diff --git a/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayPanelViewController.java b/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayPanelViewController.java index 0fe985684543..45808a8a0b3e 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayPanelViewController.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayPanelViewController.java @@ -375,10 +375,10 @@ public abstract class OverlayPanelViewController extends OverlayViewController { } if (visible && !getOverlayViewGlobalStateController().isWindowVisible()) { - getOverlayViewGlobalStateController().setWindowVisible(true); + getOverlayViewGlobalStateController().showView(/* panelViewController= */ this); } if (!visible && getOverlayViewGlobalStateController().isWindowVisible()) { - getOverlayViewGlobalStateController().setWindowVisible(false); + getOverlayViewGlobalStateController().hideView(/* panelViewController= */ this); } getLayout().setVisibility(visible ? View.VISIBLE : View.INVISIBLE); getOverlayViewGlobalStateController().setWindowFocusable(visible); diff --git a/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayViewController.java b/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayViewController.java index 87f20208476b..30e26578bd73 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayViewController.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayViewController.java @@ -54,7 +54,6 @@ public class OverlayViewController { mOverlayViewGlobalStateController.hideView(/* viewController= */ this, this::hide); } - /** * Inflate layout owned by controller. */ @@ -72,7 +71,7 @@ public class OverlayViewController { } /** - * Returns [@code true} if layout owned by controller has been inflated. + * Returns {@code true} if layout owned by controller has been inflated. */ public final boolean isInflated() { return mLayout != null; @@ -125,4 +124,18 @@ public class OverlayViewController { protected final OverlayViewGlobalStateController getOverlayViewGlobalStateController() { return mOverlayViewGlobalStateController; } + + /** + * Returns {@code true} if heads up notifications should be displayed over this view. + */ + protected boolean shouldShowHUN() { + return true; + } + + /** + * Returns {@code true} if navigation bar should be displayed over this view. + */ + protected boolean shouldShowNavigationBar() { + return false; + } } diff --git a/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayViewGlobalStateController.java b/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayViewGlobalStateController.java index 290505f5042a..70260b0d4cef 100644 --- a/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayViewGlobalStateController.java +++ b/packages/CarSystemUI/src/com/android/systemui/car/window/OverlayViewGlobalStateController.java @@ -16,14 +16,17 @@ package com.android.systemui.car.window; +import android.annotation.Nullable; import android.util.Log; import androidx.annotation.VisibleForTesting; import com.android.systemui.car.navigationbar.CarNavigationBarController; -import java.util.HashSet; -import java.util.Set; +import java.util.HashMap; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; import javax.inject.Inject; import javax.inject.Singleton; @@ -39,11 +42,17 @@ import javax.inject.Singleton; */ @Singleton public class OverlayViewGlobalStateController { + private static final boolean DEBUG = false; private static final String TAG = OverlayViewGlobalStateController.class.getSimpleName(); + private static final int UNKNOWN_Z_ORDER = -1; private final SystemUIOverlayWindowController mSystemUIOverlayWindowController; private final CarNavigationBarController mCarNavigationBarController; @VisibleForTesting - Set<String> mShownSet; + Map<OverlayViewController, Integer> mZOrderMap; + @VisibleForTesting + SortedMap<Integer, OverlayViewController> mZOrderVisibleSortedMap; + @VisibleForTesting + OverlayViewController mHighestZOrder; @Inject public OverlayViewGlobalStateController( @@ -52,7 +61,8 @@ public class OverlayViewGlobalStateController { mSystemUIOverlayWindowController = systemUIOverlayWindowController; mSystemUIOverlayWindowController.attach(); mCarNavigationBarController = carNavigationBarController; - mShownSet = new HashSet<>(); + mZOrderMap = new HashMap<>(); + mZOrderVisibleSortedMap = new TreeMap<>(); } /** @@ -66,51 +76,127 @@ public class OverlayViewGlobalStateController { } /** - * Show content in Overlay Window. + * Show content in Overlay Window using {@link OverlayPanelViewController}. + * + * This calls {@link OverlayViewGlobalStateController#showView(OverlayViewController, Runnable)} + * where the runnable is nullified since the actual showing of the panel is handled by the + * controller itself. */ - public void showView(OverlayViewController viewController, Runnable show) { - if (mShownSet.isEmpty()) { - mCarNavigationBarController.hideBars(); + public void showView(OverlayPanelViewController panelViewController) { + showView(panelViewController, /* show= */ null); + } + + /** + * Show content in Overlay Window using {@link OverlayViewController}. + */ + public void showView(OverlayViewController viewController, @Nullable Runnable show) { + debugLog(); + if (mZOrderVisibleSortedMap.isEmpty()) { setWindowVisible(true); } + if (!(viewController instanceof OverlayPanelViewController)) { + inflateView(viewController); + } - inflateView(viewController); + if (show != null) { + show.run(); + } - show.run(); - mShownSet.add(viewController.getClass().getName()); + updateInternalsWhenShowingView(viewController); + refreshNavigationBarVisibility(); Log.d(TAG, "Content shown: " + viewController.getClass().getName()); + debugLog(); + } + + private void updateInternalsWhenShowingView(OverlayViewController viewController) { + int zOrder; + if (mZOrderMap.containsKey(viewController)) { + zOrder = mZOrderMap.get(viewController); + } else { + zOrder = mSystemUIOverlayWindowController.getBaseLayout().indexOfChild( + viewController.getLayout()); + mZOrderMap.put(viewController, zOrder); + } + + mZOrderVisibleSortedMap.put(zOrder, viewController); + + refreshHighestZOrderWhenShowingView(viewController); + } + + private void refreshHighestZOrderWhenShowingView(OverlayViewController viewController) { + if (mZOrderMap.getOrDefault(mHighestZOrder, UNKNOWN_Z_ORDER) < mZOrderMap.get( + viewController)) { + mHighestZOrder = viewController; + } + } + + /** + * Hide content in Overlay Window using {@link OverlayPanelViewController}. + * + * This calls {@link OverlayViewGlobalStateController#hideView(OverlayViewController, Runnable)} + * where the runnable is nullified since the actual hiding of the panel is handled by the + * controller itself. + */ + public void hideView(OverlayPanelViewController panelViewController) { + hideView(panelViewController, /* hide= */ null); } /** - * Hide content in Overlay Window. + * Hide content in Overlay Window using {@link OverlayViewController}. */ - public void hideView(OverlayViewController viewController, Runnable hide) { + public void hideView(OverlayViewController viewController, @Nullable Runnable hide) { + debugLog(); if (!viewController.isInflated()) { Log.d(TAG, "Content cannot be hidden since it isn't inflated: " + viewController.getClass().getName()); return; } - if (!mShownSet.contains(viewController.getClass().getName())) { - Log.d(TAG, "Content cannot be hidden since it isn't shown: " + if (!mZOrderMap.containsKey(viewController)) { + Log.d(TAG, "Content cannot be hidden since it has never been shown: " + + viewController.getClass().getName()); + return; + } + if (!mZOrderVisibleSortedMap.containsKey(mZOrderMap.get(viewController))) { + Log.d(TAG, "Content cannot be hidden since it isn't currently shown: " + viewController.getClass().getName()); return; } - hide.run(); - mShownSet.remove(viewController.getClass().getName()); + if (hide != null) { + hide.run(); + } - if (mShownSet.isEmpty()) { - mCarNavigationBarController.showBars(); + mZOrderVisibleSortedMap.remove(mZOrderMap.get(viewController)); + refreshHighestZOrderWhenHidingView(viewController); + refreshNavigationBarVisibility(); + + if (mZOrderVisibleSortedMap.isEmpty()) { setWindowVisible(false); } Log.d(TAG, "Content hidden: " + viewController.getClass().getName()); + debugLog(); + } + + private void refreshHighestZOrderWhenHidingView(OverlayViewController viewController) { + if (mZOrderVisibleSortedMap.isEmpty()) { + mHighestZOrder = null; + return; + } + if (!mHighestZOrder.equals(viewController)) { + return; + } + + mHighestZOrder = mZOrderVisibleSortedMap.get(mZOrderVisibleSortedMap.lastKey()); } - /** Sets the window visibility state. */ - public void setWindowVisible(boolean expanded) { - mSystemUIOverlayWindowController.setWindowVisible(expanded); + private void refreshNavigationBarVisibility() { + if (mZOrderVisibleSortedMap.isEmpty() || mHighestZOrder.shouldShowNavigationBar()) { + mCarNavigationBarController.showBars(); + } else { + mCarNavigationBarController.hideBars(); + } } /** Returns {@code true} is the window is visible. */ @@ -118,13 +204,14 @@ public class OverlayViewGlobalStateController { return mSystemUIOverlayWindowController.isWindowVisible(); } - /** Sets the focusable flag of the sysui overlawy window. */ - public void setWindowFocusable(boolean focusable) { - mSystemUIOverlayWindowController.setWindowFocusable(focusable); + private void setWindowVisible(boolean visible) { + mSystemUIOverlayWindowController.setWindowVisible(visible); } - /** Sets the {@link android.view.WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM} flag of the - * sysui overlay window */ + /** + * Sets the {@link android.view.WindowManager.LayoutParams#FLAG_ALT_FOCUSABLE_IM} flag of the + * sysui overlay window. + */ public void setWindowNeedsInput(boolean needsInput) { mSystemUIOverlayWindowController.setWindowNeedsInput(needsInput); } @@ -134,10 +221,34 @@ public class OverlayViewGlobalStateController { return mSystemUIOverlayWindowController.isWindowFocusable(); } + /** Sets the focusable flag of the sysui overlawy window. */ + public void setWindowFocusable(boolean focusable) { + mSystemUIOverlayWindowController.setWindowFocusable(focusable); + } + /** Inflates the view controlled by the given view controller. */ public void inflateView(OverlayViewController viewController) { if (!viewController.isInflated()) { viewController.inflate(mSystemUIOverlayWindowController.getBaseLayout()); } } + + /** + * Return {@code true} if OverlayWindow is in a state where HUNs should be displayed above it. + */ + public boolean shouldShowHUN() { + return mZOrderVisibleSortedMap.isEmpty() || mHighestZOrder.shouldShowHUN(); + } + + private void debugLog() { + if (!DEBUG) { + return; + } + + Log.d(TAG, "mHighestZOrder: " + mHighestZOrder); + Log.d(TAG, "mZOrderVisibleSortedMap.size(): " + mZOrderVisibleSortedMap.size()); + Log.d(TAG, "mZOrderVisibleSortedMap: " + mZOrderVisibleSortedMap); + Log.d(TAG, "mZOrderMap.size(): " + mZOrderMap.size()); + Log.d(TAG, "mZOrderMap: " + mZOrderMap); + } } diff --git a/packages/CarSystemUI/tests/res/layout/overlay_view_global_state_controller_test.xml b/packages/CarSystemUI/tests/res/layout/overlay_view_global_state_controller_test.xml new file mode 100644 index 000000000000..03fe0e4fcf2e --- /dev/null +++ b/packages/CarSystemUI/tests/res/layout/overlay_view_global_state_controller_test.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- Fullscreen views in sysui should be listed here in increasing Z order. --> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:background="@android:color/transparent" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ViewStub android:id="@+id/overlay_view_controller_stub_1" + android:inflatedId="@+id/overlay_view_controller_1" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout="@layout/overlay_view_controller_stub"/> + + <ViewStub android:id="@+id/overlay_view_controller_stub_2" + android:inflatedId="@+id/overlay_view_controller_2" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout="@layout/overlay_view_controller_stub"/> + + <ViewStub android:id="@+id/overlay_view_controller_stub_3" + android:inflatedId="@+id/overlay_view_controller_3" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout="@layout/overlay_view_controller_stub"/> + +</FrameLayout>
\ No newline at end of file diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/keyguard/CarKeyguardViewControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/keyguard/CarKeyguardViewControllerTest.java index a2192af14758..1b4621f1c279 100644 --- a/packages/CarSystemUI/tests/src/com/android/systemui/car/keyguard/CarKeyguardViewControllerTest.java +++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/keyguard/CarKeyguardViewControllerTest.java @@ -16,9 +16,10 @@ package com.android.systemui.car.keyguard; -import static com.google.common.truth.Truth.assertThat; - +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -29,7 +30,6 @@ import android.os.Handler; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.LayoutInflater; -import android.view.View; import android.view.ViewGroup; import com.android.internal.widget.LockPatternUtils; @@ -40,7 +40,6 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.car.CarServiceProvider; import com.android.systemui.car.navigationbar.CarNavigationBarController; import com.android.systemui.car.window.OverlayViewGlobalStateController; -import com.android.systemui.car.window.SystemUIOverlayWindowController; import com.android.systemui.keyguard.DismissCallbackRegistry; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.statusbar.phone.BiometricUnlockController; @@ -51,6 +50,7 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -61,28 +61,20 @@ import dagger.Lazy; public class CarKeyguardViewControllerTest extends SysuiTestCase { private TestableCarKeyguardViewController mCarKeyguardViewController; - private OverlayViewGlobalStateController mOverlayViewGlobalStateController; - private ViewGroup mBaseLayout; @Mock + private OverlayViewGlobalStateController mOverlayViewGlobalStateController; + @Mock private KeyguardBouncer mBouncer; @Mock private CarNavigationBarController mCarNavigationBarController; @Mock - private SystemUIOverlayWindowController mSystemUIOverlayWindowController; - @Mock private CarKeyguardViewController.OnKeyguardCancelClickedListener mCancelClickedListener; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mOverlayViewGlobalStateController = new OverlayViewGlobalStateController( - mCarNavigationBarController, mSystemUIOverlayWindowController); - mBaseLayout = (ViewGroup) LayoutInflater.from(mContext).inflate( - R.layout.sysui_overlay_window, /* root= */ null); - when(mSystemUIOverlayWindowController.getBaseLayout()).thenReturn(mBaseLayout); - mCarKeyguardViewController = new TestableCarKeyguardViewController( mContext, Handler.getMain(), @@ -98,6 +90,8 @@ public class CarKeyguardViewControllerTest extends SysuiTestCase { mock(FalsingManager.class), () -> mock(KeyguardBypassController.class) ); + mCarKeyguardViewController.inflate((ViewGroup) LayoutInflater.from(mContext).inflate( + R.layout.sysui_overlay_window, /* root= */ null)); } @Test @@ -113,8 +107,7 @@ public class CarKeyguardViewControllerTest extends SysuiTestCase { when(mBouncer.isSecure()).thenReturn(true); mCarKeyguardViewController.show(/* options= */ null); - assertThat(mBaseLayout.findViewById(R.id.keyguard_container).getVisibility()).isEqualTo( - View.VISIBLE); + verify(mOverlayViewGlobalStateController).showView(eq(mCarKeyguardViewController), any()); } @Test @@ -130,8 +123,17 @@ public class CarKeyguardViewControllerTest extends SysuiTestCase { when(mBouncer.isSecure()).thenReturn(false); mCarKeyguardViewController.show(/* options= */ null); - assertThat(mBaseLayout.findViewById(R.id.keyguard_container).getVisibility()).isEqualTo( - View.GONE); + // Here we check for both showView and hideView since the current implementation of show + // with bouncer being not secure has the following method execution orders: + // 1) show -> start -> showView + // 2) show -> reset -> dismissAndCollapse -> hide -> stop -> hideView + // Hence, we want to make sure that showView is called before hideView and not in any + // other combination. + InOrder inOrder = inOrder(mOverlayViewGlobalStateController); + inOrder.verify(mOverlayViewGlobalStateController).showView(eq(mCarKeyguardViewController), + any()); + inOrder.verify(mOverlayViewGlobalStateController).hideView(eq(mCarKeyguardViewController), + any()); } @Test @@ -156,8 +158,11 @@ public class CarKeyguardViewControllerTest extends SysuiTestCase { mCarKeyguardViewController.show(/* options= */ null); mCarKeyguardViewController.hide(/* startTime= */ 0, /* fadeoutDelay= */ 0); - assertThat(mBaseLayout.findViewById(R.id.keyguard_container).getVisibility()).isEqualTo( - View.GONE); + InOrder inOrder = inOrder(mOverlayViewGlobalStateController); + inOrder.verify(mOverlayViewGlobalStateController).showView(eq(mCarKeyguardViewController), + any()); + inOrder.verify(mOverlayViewGlobalStateController).hideView(eq(mCarKeyguardViewController), + any()); } @Test diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java index 6ac72a681bfe..ccaeb458fe54 100644 --- a/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java +++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java @@ -28,9 +28,9 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; -import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.car.CarDeviceProvisionedController; +import com.android.systemui.car.window.OverlayViewGlobalStateController; import org.junit.Before; import org.junit.Test; @@ -42,12 +42,11 @@ import org.mockito.MockitoAnnotations; @TestableLooper.RunWithLooper @SmallTest public class CarHeadsUpNotificationSystemContainerTest extends SysuiTestCase { - private CarHeadsUpNotificationSystemContainer mDefaultController; - private CarHeadsUpNotificationSystemContainer mOverrideEnabledController; + private CarHeadsUpNotificationSystemContainer mCarHeadsUpNotificationSystemContainer; @Mock private CarDeviceProvisionedController mCarDeviceProvisionedController; @Mock - private NotificationPanelViewController mNotificationPanelViewController; + private OverlayViewGlobalStateController mOverlayViewGlobalStateController; @Mock private WindowManager mWindowManager; @@ -58,76 +57,63 @@ public class CarHeadsUpNotificationSystemContainerTest extends SysuiTestCase { @Before public void setUp() { - MockitoAnnotations.initMocks(this); + MockitoAnnotations.initMocks(/* testClass= */this); - when(mNotificationPanelViewController.isPanelExpanded()).thenReturn(false); - when(mCarDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true); - when(mCarDeviceProvisionedController.isCurrentUserSetupInProgress()).thenReturn(false); + when(mOverlayViewGlobalStateController.shouldShowHUN()).thenReturn(true); + when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(true); TestableResources testableResources = mContext.getOrCreateTestableResources(); - testableResources.addOverride( - R.bool.config_enableHeadsUpNotificationWhenNotificationShadeOpen, false); - - mDefaultController = new CarHeadsUpNotificationSystemContainer(mContext, - testableResources.getResources(), mCarDeviceProvisionedController, mWindowManager, - () -> mNotificationPanelViewController); - - testableResources.addOverride( - R.bool.config_enableHeadsUpNotificationWhenNotificationShadeOpen, true); - - mOverrideEnabledController = new CarHeadsUpNotificationSystemContainer(mContext, + mCarHeadsUpNotificationSystemContainer = new CarHeadsUpNotificationSystemContainer(mContext, testableResources.getResources(), mCarDeviceProvisionedController, mWindowManager, - () -> mNotificationPanelViewController); + mOverlayViewGlobalStateController); } @Test public void testDisplayNotification_firstNotification_isVisible() { - mDefaultController.displayNotification(mNotificationView); - assertThat(mDefaultController.isVisible()).isTrue(); + mCarHeadsUpNotificationSystemContainer.displayNotification(mNotificationView); + assertThat(mCarHeadsUpNotificationSystemContainer.isVisible()).isTrue(); } @Test public void testRemoveNotification_lastNotification_isInvisible() { - mDefaultController.displayNotification(mNotificationView); - mDefaultController.removeNotification(mNotificationView); - assertThat(mDefaultController.isVisible()).isFalse(); + mCarHeadsUpNotificationSystemContainer.displayNotification(mNotificationView); + mCarHeadsUpNotificationSystemContainer.removeNotification(mNotificationView); + assertThat(mCarHeadsUpNotificationSystemContainer.isVisible()).isFalse(); } @Test public void testRemoveNotification_nonLastNotification_isVisible() { - mDefaultController.displayNotification(mNotificationView); - mDefaultController.displayNotification(mNotificationView2); - mDefaultController.removeNotification(mNotificationView); - assertThat(mDefaultController.isVisible()).isTrue(); + mCarHeadsUpNotificationSystemContainer.displayNotification(mNotificationView); + mCarHeadsUpNotificationSystemContainer.displayNotification(mNotificationView2); + mCarHeadsUpNotificationSystemContainer.removeNotification(mNotificationView); + assertThat(mCarHeadsUpNotificationSystemContainer.isVisible()).isTrue(); } @Test - public void testDisplayNotification_userSetupInProgress_isInvisible() { - when(mCarDeviceProvisionedController.isCurrentUserSetupInProgress()).thenReturn(true); - mDefaultController.displayNotification(mNotificationView); - assertThat(mDefaultController.isVisible()).isFalse(); + public void testDisplayNotification_userFullySetupTrue_isInvisible() { + mCarHeadsUpNotificationSystemContainer.displayNotification(mNotificationView); + assertThat(mCarHeadsUpNotificationSystemContainer.isVisible()).isTrue(); } @Test - public void testDisplayNotification_userSetupIncomplete_isInvisible() { - when(mCarDeviceProvisionedController.isCurrentUserSetup()).thenReturn(false); - mDefaultController.displayNotification(mNotificationView); - assertThat(mDefaultController.isVisible()).isFalse(); + public void testDisplayNotification_userFullySetupFalse_isInvisible() { + when(mCarDeviceProvisionedController.isCurrentUserFullySetup()).thenReturn(false); + mCarHeadsUpNotificationSystemContainer.displayNotification(mNotificationView); + assertThat(mCarHeadsUpNotificationSystemContainer.isVisible()).isFalse(); } @Test - public void testDisplayNotification_notificationPanelExpanded_isInvisible() { - when(mNotificationPanelViewController.isPanelExpanded()).thenReturn(true); - mDefaultController.displayNotification(mNotificationView); - assertThat(mDefaultController.isVisible()).isFalse(); + public void testDisplayNotification_overlayWindowStateShouldShowHUNFalse_isInvisible() { + when(mOverlayViewGlobalStateController.shouldShowHUN()).thenReturn(false); + mCarHeadsUpNotificationSystemContainer.displayNotification(mNotificationView); + assertThat(mCarHeadsUpNotificationSystemContainer.isVisible()).isFalse(); } @Test - public void testDisplayNotification_notificationPanelExpandedEnabledHUNWhenOpen_isVisible() { - when(mNotificationPanelViewController.isPanelExpanded()).thenReturn(true); - mOverrideEnabledController.displayNotification(mNotificationView); - assertThat(mOverrideEnabledController.isVisible()).isTrue(); + public void testDisplayNotification_overlayWindowStateShouldShowHUNTrue_isVisible() { + mCarHeadsUpNotificationSystemContainer.displayNotification(mNotificationView); + assertThat(mCarHeadsUpNotificationSystemContainer.isVisible()).isTrue(); } } diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayPanelViewControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayPanelViewControllerTest.java index 8d705a8cca1f..45a05ac69bd7 100644 --- a/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayPanelViewControllerTest.java +++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayPanelViewControllerTest.java @@ -339,7 +339,7 @@ public class OverlayPanelViewControllerTest extends SysuiTestCase { mOverlayPanelViewController.setPanelVisible(true); - verify(mOverlayViewGlobalStateController).setWindowVisible(true); + verify(mOverlayViewGlobalStateController).showView(mOverlayPanelViewController); } @Test @@ -349,7 +349,7 @@ public class OverlayPanelViewControllerTest extends SysuiTestCase { mOverlayPanelViewController.setPanelVisible(true); - verify(mOverlayViewGlobalStateController, never()).setWindowVisible(true); + verify(mOverlayViewGlobalStateController, never()).showView(mOverlayPanelViewController); } @Test @@ -377,7 +377,7 @@ public class OverlayPanelViewControllerTest extends SysuiTestCase { mOverlayPanelViewController.setPanelVisible(false); - verify(mOverlayViewGlobalStateController).setWindowVisible(false); + verify(mOverlayViewGlobalStateController).hideView(mOverlayPanelViewController); } @Test @@ -387,7 +387,7 @@ public class OverlayPanelViewControllerTest extends SysuiTestCase { mOverlayPanelViewController.setPanelVisible(false); - verify(mOverlayViewGlobalStateController, never()).setWindowVisible(false); + verify(mOverlayViewGlobalStateController, never()).hideView(mOverlayPanelViewController); } @Test @@ -428,10 +428,6 @@ public class OverlayPanelViewControllerTest extends SysuiTestCase { private static class TestOverlayPanelViewController extends OverlayPanelViewController { - private boolean mShouldAnimateCollapsePanel; - private boolean mShouldAnimateExpandPanel; - private boolean mShouldAllowClosingScroll; - boolean mOnAnimateCollapsePanelCalled; boolean mAnimateCollapsePanelCalled; boolean mOnAnimateExpandPanelCalled; @@ -440,6 +436,9 @@ public class OverlayPanelViewControllerTest extends SysuiTestCase { boolean mOnExpandAnimationEndCalled; boolean mOnOpenScrollStartEnd; List<Integer> mOnScrollHeights; + private boolean mShouldAnimateCollapsePanel; + private boolean mShouldAnimateExpandPanel; + private boolean mShouldAllowClosingScroll; TestOverlayPanelViewController( Context context, diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayViewGlobalStateControllerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayViewGlobalStateControllerTest.java index 25dd4f502fb7..9e6e616e3ccf 100644 --- a/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayViewGlobalStateControllerTest.java +++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/window/OverlayViewGlobalStateControllerTest.java @@ -24,25 +24,33 @@ import static org.mockito.Mockito.when; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; -import android.widget.FrameLayout; +import android.view.ViewStub; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.car.navigationbar.CarNavigationBarController; +import com.android.systemui.tests.R; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.Arrays; + @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @SmallTest public class OverlayViewGlobalStateControllerTest extends SysuiTestCase { - private static final String MOCK_OVERLAY_VIEW_CONTROLLER_NAME = "OverlayViewController"; + private static final int OVERLAY_VIEW_CONTROLLER_1_Z_ORDER = 0; + private static final int OVERLAY_VIEW_CONTROLLER_2_Z_ORDER = 1; + private static final int OVERLAY_PANEL_VIEW_CONTROLLER_Z_ORDER = 2; private OverlayViewGlobalStateController mOverlayViewGlobalStateController; private ViewGroup mBaseLayout; @@ -54,7 +62,11 @@ public class OverlayViewGlobalStateControllerTest extends SysuiTestCase { @Mock private OverlayViewMediator mOverlayViewMediator; @Mock - private OverlayViewController mOverlayViewController; + private OverlayViewController mOverlayViewController1; + @Mock + private OverlayViewController mOverlayViewController2; + @Mock + private OverlayPanelViewController mOverlayPanelViewController; @Mock private Runnable mRunnable; @@ -62,14 +74,15 @@ public class OverlayViewGlobalStateControllerTest extends SysuiTestCase { public void setUp() { MockitoAnnotations.initMocks(/* testClass= */ this); + mBaseLayout = (ViewGroup) LayoutInflater.from(mContext).inflate( + R.layout.overlay_view_global_state_controller_test, /* root= */ null); + + when(mSystemUIOverlayWindowController.getBaseLayout()).thenReturn(mBaseLayout); + mOverlayViewGlobalStateController = new OverlayViewGlobalStateController( mCarNavigationBarController, mSystemUIOverlayWindowController); verify(mSystemUIOverlayWindowController).attach(); - - mBaseLayout = new FrameLayout(mContext); - - when(mSystemUIOverlayWindowController.getBaseLayout()).thenReturn(mBaseLayout); } @Test @@ -87,182 +100,445 @@ public class OverlayViewGlobalStateControllerTest extends SysuiTestCase { } @Test - public void showView_nothingAlreadyShown_navigationBarsHidden() { - mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable); + public void showView_nothingAlreadyShown_shouldShowNavBarFalse_navigationBarsHidden() { + setupOverlayViewController1(); + when(mOverlayViewController1.shouldShowNavigationBar()).thenReturn(false); + + mOverlayViewGlobalStateController.showView(mOverlayViewController1, mRunnable); verify(mCarNavigationBarController).hideBars(); } @Test - public void showView_nothingAlreadyShown_windowIsExpanded() { - mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable); + public void showView_nothingAlreadyShown_shouldShowNavBarTrue_navigationBarsShown() { + setupOverlayViewController1(); + when(mOverlayViewController1.shouldShowNavigationBar()).thenReturn(true); + + mOverlayViewGlobalStateController.showView(mOverlayViewController1, mRunnable); + + verify(mCarNavigationBarController).showBars(); + } + + @Test + public void showView_nothingAlreadyShown_windowIsSetVisible() { + setupOverlayViewController1(); + + mOverlayViewGlobalStateController.showView(mOverlayViewController1, mRunnable); verify(mSystemUIOverlayWindowController).setWindowVisible(true); } @Test - public void showView_somethingAlreadyShown_navigationBarsHidden() { - mOverlayViewGlobalStateController.mShownSet.add(MOCK_OVERLAY_VIEW_CONTROLLER_NAME); + public void showView_nothingAlreadyShown_newHighestZOrder() { + setupOverlayViewController1(); + + mOverlayViewGlobalStateController.showView(mOverlayViewController1, mRunnable); + + assertThat(mOverlayViewGlobalStateController.mHighestZOrder).isEqualTo( + mOverlayViewController1); + } + + @Test + public void showView_nothingAlreadyShown_newHighestZOrder_isVisible() { + setupOverlayViewController1(); + + mOverlayViewGlobalStateController.showView(mOverlayViewController1, mRunnable); + + assertThat(mOverlayViewGlobalStateController.mZOrderVisibleSortedMap.containsKey( + OVERLAY_VIEW_CONTROLLER_1_Z_ORDER)).isTrue(); + } + + @Test + public void showView_newHighestZOrder() { + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + setupOverlayViewController2(); - mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable); + mOverlayViewGlobalStateController.showView(mOverlayViewController2, mRunnable); - verify(mCarNavigationBarController, never()).hideBars(); + assertThat(mOverlayViewGlobalStateController.mHighestZOrder).isEqualTo( + mOverlayViewController2); } @Test - public void showView_somethingAlreadyShown_windowIsExpanded() { - mOverlayViewGlobalStateController.mShownSet.add(MOCK_OVERLAY_VIEW_CONTROLLER_NAME); + public void showView_newHighestZOrder_shouldShowNavBarFalse_navigationBarsHidden() { + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + setupOverlayViewController2(); + when(mOverlayViewController2.shouldShowNavigationBar()).thenReturn(false); + + mOverlayViewGlobalStateController.showView(mOverlayViewController2, mRunnable); - mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable); + verify(mCarNavigationBarController).hideBars(); + } + + @Test + public void showView_newHighestZOrder_shouldShowNavBarTrue_navigationBarsShown() { + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + setupOverlayViewController2(); + when(mOverlayViewController2.shouldShowNavigationBar()).thenReturn(true); + + mOverlayViewGlobalStateController.showView(mOverlayViewController2, mRunnable); + + verify(mCarNavigationBarController).showBars(); + } + + @Test + public void showView_newHighestZOrder_correctViewsShown() { + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + setupOverlayViewController2(); + + mOverlayViewGlobalStateController.showView(mOverlayViewController2, mRunnable); + + assertThat(mOverlayViewGlobalStateController.mZOrderVisibleSortedMap.keySet().toArray()) + .isEqualTo(Arrays.asList(OVERLAY_VIEW_CONTROLLER_1_Z_ORDER, + OVERLAY_VIEW_CONTROLLER_2_Z_ORDER).toArray()); + } + + @Test + public void showView_oldHighestZOrder() { + setupOverlayViewController2(); + setOverlayViewControllerAsShowing(mOverlayViewController2); + + mOverlayViewGlobalStateController.showView(mOverlayViewController1, mRunnable); + + assertThat(mOverlayViewGlobalStateController.mHighestZOrder).isEqualTo( + mOverlayViewController2); + } + + @Test + public void showView_oldHighestZOrder_shouldShowNavBarFalse_navigationBarsHidden() { + setupOverlayViewController2(); + setOverlayViewControllerAsShowing(mOverlayViewController2); + when(mOverlayViewController1.shouldShowNavigationBar()).thenReturn(true); + when(mOverlayViewController2.shouldShowNavigationBar()).thenReturn(false); + + mOverlayViewGlobalStateController.showView(mOverlayViewController1, mRunnable); + + verify(mCarNavigationBarController).hideBars(); + } + + @Test + public void showView_oldHighestZOrder_shouldShowNavBarTrue_navigationBarsShown() { + setupOverlayViewController2(); + setOverlayViewControllerAsShowing(mOverlayViewController2); + when(mOverlayViewController1.shouldShowNavigationBar()).thenReturn(false); + when(mOverlayViewController2.shouldShowNavigationBar()).thenReturn(true); + + mOverlayViewGlobalStateController.showView(mOverlayViewController1, mRunnable); + + verify(mCarNavigationBarController).showBars(); + } + + @Test + public void showView_oldHighestZOrder_correctViewsShown() { + setupOverlayViewController1(); + setupOverlayViewController2(); + setOverlayViewControllerAsShowing(mOverlayViewController2); + + mOverlayViewGlobalStateController.showView(mOverlayViewController1, mRunnable); + + assertThat(mOverlayViewGlobalStateController.mZOrderVisibleSortedMap.keySet().toArray()) + .isEqualTo(Arrays.asList(OVERLAY_VIEW_CONTROLLER_1_Z_ORDER, + OVERLAY_VIEW_CONTROLLER_2_Z_ORDER).toArray()); + } + + @Test + public void showView_somethingAlreadyShown_windowVisibleNotCalled() { + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + setupOverlayViewController2(); + + mOverlayViewGlobalStateController.showView(mOverlayViewController2, mRunnable); verify(mSystemUIOverlayWindowController, never()).setWindowVisible(true); } @Test public void showView_viewControllerNotInflated_inflateViewController() { - when(mOverlayViewController.isInflated()).thenReturn(false); + setupOverlayViewController2(); + when(mOverlayViewController2.isInflated()).thenReturn(false); - mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable); + mOverlayViewGlobalStateController.showView(mOverlayViewController2, mRunnable); - verify(mOverlayViewController).inflate(mBaseLayout); + verify(mOverlayViewController2).inflate(mBaseLayout); } @Test public void showView_viewControllerInflated_inflateViewControllerNotCalled() { - when(mOverlayViewController.isInflated()).thenReturn(true); + setupOverlayViewController2(); - mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable); + mOverlayViewGlobalStateController.showView(mOverlayViewController2, mRunnable); - verify(mOverlayViewController, never()).inflate(mBaseLayout); + verify(mOverlayViewController2, never()).inflate(mBaseLayout); } @Test - public void showView_showRunnableCalled() { - mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable); + public void showView_panelViewController_inflateViewControllerNotCalled() { + setupOverlayPanelViewController(); - verify(mRunnable).run(); + mOverlayViewGlobalStateController.showView(mOverlayPanelViewController, mRunnable); + + verify(mOverlayPanelViewController, never()).inflate(mBaseLayout); + verify(mOverlayPanelViewController, never()).isInflated(); } @Test - public void showView_overlayViewControllerAddedToShownSet() { - mOverlayViewGlobalStateController.showView(mOverlayViewController, mRunnable); + public void showView_showRunnableCalled() { + setupOverlayViewController1(); + + mOverlayViewGlobalStateController.showView(mOverlayViewController1, mRunnable); - assertThat(mOverlayViewGlobalStateController.mShownSet.contains( - mOverlayViewController.getClass().getName())).isTrue(); + verify(mRunnable).run(); } @Test public void hideView_viewControllerNotInflated_hideRunnableNotCalled() { - when(mOverlayViewController.isInflated()).thenReturn(false); + when(mOverlayViewController2.isInflated()).thenReturn(false); - mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable); + mOverlayViewGlobalStateController.hideView(mOverlayViewController2, mRunnable); verify(mRunnable, never()).run(); } @Test public void hideView_nothingShown_hideRunnableNotCalled() { - when(mOverlayViewController.isInflated()).thenReturn(true); - mOverlayViewGlobalStateController.mShownSet.clear(); + when(mOverlayViewController2.isInflated()).thenReturn(true); + mOverlayViewGlobalStateController.mZOrderMap.clear(); - mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable); + mOverlayViewGlobalStateController.hideView(mOverlayViewController2, mRunnable); verify(mRunnable, never()).run(); } @Test public void hideView_viewControllerNotShown_hideRunnableNotCalled() { - when(mOverlayViewController.isInflated()).thenReturn(true); - mOverlayViewGlobalStateController.mShownSet.add(MOCK_OVERLAY_VIEW_CONTROLLER_NAME); + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + when(mOverlayViewController2.isInflated()).thenReturn(true); - mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable); + mOverlayViewGlobalStateController.hideView(mOverlayViewController2, mRunnable); verify(mRunnable, never()).run(); } @Test public void hideView_viewControllerShown_hideRunnableCalled() { - when(mOverlayViewController.isInflated()).thenReturn(true); - mOverlayViewGlobalStateController.mShownSet.add( - mOverlayViewController.getClass().getName()); + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); - mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable); + mOverlayViewGlobalStateController.hideView(mOverlayViewController1, mRunnable); verify(mRunnable).run(); } @Test + public void hideView_viewControllerOnlyShown_noHighestZOrder() { + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + + mOverlayViewGlobalStateController.hideView(mOverlayViewController1, mRunnable); + + assertThat(mOverlayViewGlobalStateController.mHighestZOrder).isNull(); + } + + @Test public void hideView_viewControllerOnlyShown_nothingShown() { - when(mOverlayViewController.isInflated()).thenReturn(true); - mOverlayViewGlobalStateController.mShownSet.add( - mOverlayViewController.getClass().getName()); + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + + mOverlayViewGlobalStateController.hideView(mOverlayViewController1, mRunnable); + + assertThat(mOverlayViewGlobalStateController.mZOrderVisibleSortedMap.isEmpty()).isTrue(); + } + + @Test + public void hideView_viewControllerOnlyShown_viewControllerNotShown() { + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + + mOverlayViewGlobalStateController.hideView(mOverlayViewController1, mRunnable); + + assertThat(mOverlayViewGlobalStateController.mZOrderVisibleSortedMap.containsKey( + OVERLAY_VIEW_CONTROLLER_1_Z_ORDER)).isFalse(); + } + + @Test + public void hideView_newHighestZOrder_twoViewsShown() { + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + setupOverlayViewController2(); + setOverlayViewControllerAsShowing(mOverlayViewController2); + + mOverlayViewGlobalStateController.hideView(mOverlayViewController2, mRunnable); + + assertThat(mOverlayViewGlobalStateController.mHighestZOrder).isEqualTo( + mOverlayViewController1); + } + + @Test + public void hideView_newHighestZOrder_threeViewsShown() { + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + setupOverlayViewController2(); + setOverlayViewControllerAsShowing(mOverlayViewController2); + setupOverlayPanelViewController(); + setOverlayViewControllerAsShowing(mOverlayPanelViewController); + + mOverlayViewGlobalStateController.hideView(mOverlayPanelViewController, mRunnable); + + assertThat(mOverlayViewGlobalStateController.mHighestZOrder).isEqualTo( + mOverlayViewController2); + } + + @Test + public void hideView_newHighestZOrder_shouldShowNavBarFalse_navigationBarHidden() { + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + setupOverlayViewController2(); + setOverlayViewControllerAsShowing(mOverlayViewController2); + when(mOverlayViewController1.shouldShowNavigationBar()).thenReturn(false); + + mOverlayViewGlobalStateController.hideView(mOverlayViewController2, mRunnable); + + verify(mCarNavigationBarController).hideBars(); + } + + @Test + public void hideView_newHighestZOrder_shouldShowNavBarTrue_navigationBarShown() { + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + setupOverlayViewController2(); + setOverlayViewControllerAsShowing(mOverlayViewController2); + when(mOverlayViewController1.shouldShowNavigationBar()).thenReturn(true); + + mOverlayViewGlobalStateController.hideView(mOverlayViewController2, mRunnable); - mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable); + verify(mCarNavigationBarController).showBars(); + } + + @Test + public void hideView_oldHighestZOrder() { + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + setupOverlayViewController2(); + setOverlayViewControllerAsShowing(mOverlayViewController2); - assertThat(mOverlayViewGlobalStateController.mShownSet.isEmpty()).isTrue(); + mOverlayViewGlobalStateController.hideView(mOverlayViewController1, mRunnable); + + assertThat(mOverlayViewGlobalStateController.mHighestZOrder).isEqualTo( + mOverlayViewController2); } @Test - public void hideView_viewControllerNotOnlyShown_navigationBarNotShown() { - when(mOverlayViewController.isInflated()).thenReturn(true); - mOverlayViewGlobalStateController.mShownSet.add( - mOverlayViewController.getClass().getName()); - mOverlayViewGlobalStateController.mShownSet.add(MOCK_OVERLAY_VIEW_CONTROLLER_NAME); + public void hideView_oldHighestZOrder_shouldShowNavBarFalse_navigationBarHidden() { + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + setupOverlayViewController2(); + setOverlayViewControllerAsShowing(mOverlayViewController2); + when(mOverlayViewController2.shouldShowNavigationBar()).thenReturn(false); - mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable); + mOverlayViewGlobalStateController.hideView(mOverlayViewController1, mRunnable); - verify(mCarNavigationBarController, never()).showBars(); + verify(mCarNavigationBarController).hideBars(); + } + + @Test + public void hideView_oldHighestZOrder_shouldShowNavBarTrue_navigationBarShown() { + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + setupOverlayViewController2(); + setOverlayViewControllerAsShowing(mOverlayViewController2); + when(mOverlayViewController2.shouldShowNavigationBar()).thenReturn(true); + + mOverlayViewGlobalStateController.hideView(mOverlayViewController1, mRunnable); + + verify(mCarNavigationBarController).showBars(); } @Test public void hideView_viewControllerNotOnlyShown_windowNotCollapsed() { - when(mOverlayViewController.isInflated()).thenReturn(true); - mOverlayViewGlobalStateController.mShownSet.add( - mOverlayViewController.getClass().getName()); - mOverlayViewGlobalStateController.mShownSet.add(MOCK_OVERLAY_VIEW_CONTROLLER_NAME); + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); + setupOverlayViewController2(); + setOverlayViewControllerAsShowing(mOverlayViewController2); - mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable); + mOverlayViewGlobalStateController.hideView(mOverlayViewController2, mRunnable); verify(mSystemUIOverlayWindowController, never()).setWindowVisible(false); } @Test public void hideView_viewControllerOnlyShown_navigationBarShown() { - when(mOverlayViewController.isInflated()).thenReturn(true); - mOverlayViewGlobalStateController.mShownSet.add( - mOverlayViewController.getClass().getName()); + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); - mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable); + mOverlayViewGlobalStateController.hideView(mOverlayViewController1, mRunnable); verify(mCarNavigationBarController).showBars(); } @Test public void hideView_viewControllerOnlyShown_windowCollapsed() { - when(mOverlayViewController.isInflated()).thenReturn(true); - mOverlayViewGlobalStateController.mShownSet.add( - mOverlayViewController.getClass().getName()); + setupOverlayViewController1(); + setOverlayViewControllerAsShowing(mOverlayViewController1); - mOverlayViewGlobalStateController.hideView(mOverlayViewController, mRunnable); + mOverlayViewGlobalStateController.hideView(mOverlayViewController1, mRunnable); verify(mSystemUIOverlayWindowController).setWindowVisible(false); } @Test public void inflateView_notInflated_inflates() { - when(mOverlayViewController.isInflated()).thenReturn(false); + when(mOverlayViewController2.isInflated()).thenReturn(false); - mOverlayViewGlobalStateController.inflateView(mOverlayViewController); + mOverlayViewGlobalStateController.inflateView(mOverlayViewController2); - verify(mOverlayViewController).inflate(mBaseLayout); + verify(mOverlayViewController2).inflate(mBaseLayout); } @Test public void inflateView_alreadyInflated_doesNotInflate() { - when(mOverlayViewController.isInflated()).thenReturn(true); + when(mOverlayViewController2.isInflated()).thenReturn(true); - mOverlayViewGlobalStateController.inflateView(mOverlayViewController); + mOverlayViewGlobalStateController.inflateView(mOverlayViewController2); + + verify(mOverlayViewController2, never()).inflate(mBaseLayout); + } + + private void setupOverlayViewController1() { + setupOverlayViewController(mOverlayViewController1, R.id.overlay_view_controller_stub_1, + R.id.overlay_view_controller_1); + } - verify(mOverlayViewController, never()).inflate(mBaseLayout); + private void setupOverlayViewController2() { + setupOverlayViewController(mOverlayViewController2, R.id.overlay_view_controller_stub_2, + R.id.overlay_view_controller_2); + } + + private void setupOverlayPanelViewController() { + setupOverlayViewController(mOverlayPanelViewController, R.id.overlay_view_controller_stub_3, + R.id.overlay_view_controller_3); + } + + private void setupOverlayViewController(OverlayViewController overlayViewController, + int stubId, int inflatedId) { + ViewStub viewStub = mBaseLayout.findViewById(stubId); + View layout; + if (viewStub == null) { + layout = mBaseLayout.findViewById(inflatedId); + } else { + layout = viewStub.inflate(); + } + when(overlayViewController.getLayout()).thenReturn(layout); + when(overlayViewController.isInflated()).thenReturn(true); + } + + private void setOverlayViewControllerAsShowing(OverlayViewController overlayViewController) { + mOverlayViewGlobalStateController.showView(overlayViewController, /* show= */ null); + Mockito.reset(mCarNavigationBarController, mSystemUIOverlayWindowController); + when(mSystemUIOverlayWindowController.getBaseLayout()).thenReturn(mBaseLayout); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java index c4ff71940d20..ae3194df28fe 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/AppUtils.java @@ -140,4 +140,15 @@ public class AppUtils { .isSystemModule(packageName); } + /** + * Returns a boolean indicating whether a given package is a mainline module. + */ + public static boolean isMainlineModule(Context context, String packageName) { + final PackageManager pm = context.getPackageManager(); + try { + return pm.getModuleInfo(packageName, 0 /* flags */) != null; + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java index 31ea5b40d756..887a49b95279 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java @@ -27,10 +27,13 @@ import android.util.Log; import androidx.annotation.IntDef; import com.android.internal.annotations.VisibleForTesting; +import com.android.settingslib.bluetooth.A2dpProfile; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.HearingAidProfile; import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfile; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -430,7 +433,8 @@ public class LocalMediaManager implements BluetoothCallback { cachedDeviceManager.findDevice(device); if (cachedDevice != null) { if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED - && !cachedDevice.isConnected()) { + && !cachedDevice.isConnected() + && isA2dpOrHearingAidDevice(cachedDevice)) { deviceCount++; cachedBluetoothDeviceList.add(cachedDevice); if (deviceCount >= MAX_DISCONNECTED_DEVICE_NUM) { @@ -454,6 +458,15 @@ public class LocalMediaManager implements BluetoothCallback { return new ArrayList<>(mDisconnectedMediaDevices); } + private boolean isA2dpOrHearingAidDevice(CachedBluetoothDevice device) { + for (LocalBluetoothProfile profile : device.getConnectableProfiles()) { + if (profile instanceof A2dpProfile || profile instanceof HearingAidProfile) { + return true; + } + } + return false; + } + @Override public void onDeviceRemoved(MediaDevice device) { if (mMediaDevices.contains(device)) { diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java index 34de1528ed9a..139a12c44e0f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java @@ -31,18 +31,14 @@ import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; import android.content.Context; -import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.media.MediaRoute2Info; import android.media.MediaRouter2Manager; import android.text.TextUtils; -import android.util.Log; import androidx.annotation.IntDef; import androidx.annotation.VisibleForTesting; -import com.android.settingslib.R; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -215,30 +211,6 @@ public abstract class MediaDevice implements Comparable<MediaDevice> { * * @return application label. */ - public String getClientAppLabel() { - final String packageName = mRouteInfo.getClientPackageName(); - if (TextUtils.isEmpty(packageName)) { - Log.d(TAG, "Client package name is empty"); - return mContext.getResources().getString(R.string.unknown); - } - try { - final PackageManager packageManager = mContext.getPackageManager(); - final String appLabel = packageManager.getApplicationLabel( - packageManager.getApplicationInfo(packageName, 0)).toString(); - if (!TextUtils.isEmpty(appLabel)) { - return appLabel; - } - } catch (PackageManager.NameNotFoundException e) { - Log.e(TAG, "unable to find " + packageName); - } - return mContext.getResources().getString(R.string.unknown); - } - - /** - * Get application label from MediaDevice. - * - * @return application label. - */ public int getDeviceType() { return mType; } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java index 42f2542e5c30..8ea5ff1bf662 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -41,7 +41,10 @@ public class PhoneMediaDevice extends MediaDevice { private static final String TAG = "PhoneMediaDevice"; - public static final String ID = "phone_media_device_id_1"; + public static final String PHONE_ID = "phone_media_device_id"; + // For 3.5 mm wired headset + public static final String WIRED_HEADSET_ID = "wired_headset_media_device_id"; + public static final String USB_HEADSET_ID = "usb_headset_media_device_id"; private String mSummary = ""; @@ -109,7 +112,25 @@ public class PhoneMediaDevice extends MediaDevice { @Override public String getId() { - return ID; + String id; + switch (mRouteInfo.getType()) { + case TYPE_WIRED_HEADSET: + case TYPE_WIRED_HEADPHONES: + id = WIRED_HEADSET_ID; + break; + case TYPE_USB_DEVICE: + case TYPE_USB_HEADSET: + case TYPE_USB_ACCESSORY: + case TYPE_DOCK: + case TYPE_HDMI: + id = USB_HEADSET_ID; + break; + case TYPE_BUILTIN_SPEAKER: + default: + id = PHONE_ID; + break; + } + return id; } @Override diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java index 77316e91bae2..365a16c50b99 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java @@ -41,6 +41,7 @@ import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; import com.android.settingslib.bluetooth.HearingAidProfile; import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfile; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter; @@ -560,6 +561,10 @@ public class LocalMediaManagerTest { mLocalMediaManager.mMediaDevices.add(device3); mLocalMediaManager.mMediaDevices.add(mLocalMediaManager.mPhoneDevice); + final List<LocalBluetoothProfile> profiles = new ArrayList<>(); + final A2dpProfile a2dpProfile = mock(A2dpProfile.class); + profiles.add(a2dpProfile); + final List<BluetoothDevice> bluetoothDevices = new ArrayList<>(); final BluetoothDevice bluetoothDevice = mock(BluetoothDevice.class); final CachedBluetoothDevice cachedDevice = mock(CachedBluetoothDevice.class); @@ -571,6 +576,7 @@ public class LocalMediaManagerTest { when(cachedManager.findDevice(bluetoothDevice)).thenReturn(cachedDevice); when(cachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); when(cachedDevice.isConnected()).thenReturn(false); + when(cachedDevice.getConnectableProfiles()).thenReturn(profiles); when(device1.getId()).thenReturn(TEST_DEVICE_ID_1); when(device2.getId()).thenReturn(TEST_DEVICE_ID_2); @@ -634,6 +640,10 @@ public class LocalMediaManagerTest { mLocalMediaManager.mMediaDevices.add(device3); mLocalMediaManager.mMediaDevices.add(mLocalMediaManager.mPhoneDevice); + final List<LocalBluetoothProfile> profiles = new ArrayList<>(); + final A2dpProfile a2dpProfile = mock(A2dpProfile.class); + profiles.add(a2dpProfile); + final List<BluetoothDevice> bluetoothDevices = new ArrayList<>(); final BluetoothDevice bluetoothDevice = mock(BluetoothDevice.class); final BluetoothDevice bluetoothDevice2 = mock(BluetoothDevice.class); @@ -662,6 +672,7 @@ public class LocalMediaManagerTest { when(cachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); when(cachedDevice.isConnected()).thenReturn(false); when(cachedDevice.getDevice()).thenReturn(bluetoothDevice); + when(cachedDevice.getConnectableProfiles()).thenReturn(profiles); when(bluetoothDevice.getBluetoothClass()).thenReturn(bluetoothClass); when(bluetoothClass.getDeviceClass()).thenReturn(AUDIO_VIDEO_HEADPHONES); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java index 6664870a6257..47d4beb752c5 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/MediaDeviceTest.java @@ -29,13 +29,9 @@ import static org.mockito.Mockito.when; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageStats; import android.media.MediaRoute2Info; import android.media.MediaRouter2Manager; -import com.android.settingslib.R; import com.android.settingslib.bluetooth.A2dpProfile; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.HearingAidProfile; @@ -49,8 +45,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import org.robolectric.Shadows; -import org.robolectric.shadows.ShadowPackageManager; import java.util.ArrayList; import java.util.Collections; @@ -70,8 +64,6 @@ public class MediaDeviceTest { private static final String ROUTER_ID_2 = "RouterId_2"; private static final String ROUTER_ID_3 = "RouterId_3"; private static final String TEST_PACKAGE_NAME = "com.test.playmusic"; - private static final String TEST_PACKAGE_NAME2 = "com.test.playmusic2"; - private static final String TEST_APPLICATION_LABEL = "playmusic"; private final BluetoothClass mHeadreeClass = new BluetoothClass(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES); private final BluetoothClass mCarkitClass = @@ -125,10 +117,6 @@ public class MediaDeviceTest { private InfoMediaDevice mInfoMediaDevice3; private List<MediaDevice> mMediaDevices = new ArrayList<>(); private PhoneMediaDevice mPhoneMediaDevice; - private ShadowPackageManager mShadowPackageManager; - private ApplicationInfo mAppInfo; - private PackageInfo mPackageInfo; - private PackageStats mPackageStats; @Before public void setUp() { @@ -459,41 +447,6 @@ public class MediaDeviceTest { assertThat(mInfoMediaDevice1.getClientPackageName()).isEqualTo(TEST_PACKAGE_NAME); } - private void initPackage() { - mShadowPackageManager = Shadows.shadowOf(mContext.getPackageManager()); - mAppInfo = new ApplicationInfo(); - mAppInfo.flags = ApplicationInfo.FLAG_INSTALLED; - mAppInfo.packageName = TEST_PACKAGE_NAME; - mAppInfo.name = TEST_APPLICATION_LABEL; - mPackageInfo = new PackageInfo(); - mPackageInfo.packageName = TEST_PACKAGE_NAME; - mPackageInfo.applicationInfo = mAppInfo; - mPackageStats = new PackageStats(TEST_PACKAGE_NAME); - } - - @Test - public void getClientAppLabel_matchedPackageName_returnLabel() { - initPackage(); - when(mRouteInfo1.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME); - - assertThat(mInfoMediaDevice1.getClientAppLabel()).isEqualTo( - mContext.getResources().getString(R.string.unknown)); - - mShadowPackageManager.addPackage(mPackageInfo, mPackageStats); - - assertThat(mInfoMediaDevice1.getClientAppLabel()).isEqualTo(TEST_APPLICATION_LABEL); - } - - @Test - public void getClientAppLabel_noMatchedPackageName_returnDefault() { - initPackage(); - mShadowPackageManager.addPackage(mPackageInfo, mPackageStats); - when(mRouteInfo1.getClientPackageName()).thenReturn(TEST_PACKAGE_NAME2); - - assertThat(mInfoMediaDevice1.getClientAppLabel()).isEqualTo( - mContext.getResources().getString(R.string.unknown)); - } - @Test public void setState_verifyGetState() { mInfoMediaDevice1.setState(LocalMediaManager.MediaDeviceState.STATE_CONNECTED); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java index 6f265dd603e5..47f6fe3bce02 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java @@ -21,6 +21,10 @@ import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; +import static com.android.settingslib.media.PhoneMediaDevice.PHONE_ID; +import static com.android.settingslib.media.PhoneMediaDevice.USB_HEADSET_ID; +import static com.android.settingslib.media.PhoneMediaDevice.WIRED_HEADSET_ID; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.when; @@ -108,4 +112,22 @@ public class PhoneMediaDeviceTest { assertThat(mPhoneMediaDevice.getName()) .isEqualTo(mContext.getString(R.string.media_transfer_this_device_name)); } + + @Test + public void getId_returnCorrectId() { + when(mInfo.getType()).thenReturn(TYPE_WIRED_HEADPHONES); + + assertThat(mPhoneMediaDevice.getId()) + .isEqualTo(WIRED_HEADSET_ID); + + when(mInfo.getType()).thenReturn(TYPE_USB_DEVICE); + + assertThat(mPhoneMediaDevice.getId()) + .isEqualTo(USB_HEADSET_ID); + + when(mInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER); + + assertThat(mPhoneMediaDevice.getId()) + .isEqualTo(PHONE_ID); + } } diff --git a/packages/SystemUI/res/drawable/screenshot_actions_background_protection.xml b/packages/SystemUI/res/drawable/screenshot_actions_background_protection.xml index 163015b7b0f0..21013c6c7b16 100644 --- a/packages/SystemUI/res/drawable/screenshot_actions_background_protection.xml +++ b/packages/SystemUI/res/drawable/screenshot_actions_background_protection.xml @@ -17,6 +17,6 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <gradient android:angle="90" - android:startColor="#1f000000" + android:startColor="@color/global_screenshot_background_protection_start" android:endColor="#00000000"/> </shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/bubble_overflow_view.xml b/packages/SystemUI/res/layout/bubble_overflow_view.xml index 88a05ec5824a..1ed1f07fb277 100644 --- a/packages/SystemUI/res/layout/bubble_overflow_view.xml +++ b/packages/SystemUI/res/layout/bubble_overflow_view.xml @@ -36,6 +36,8 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:maxLines="1" + android:lines="2" + android:ellipsize="end" android:layout_gravity="center" android:paddingTop="@dimen/bubble_overflow_text_padding" android:gravity="center"/> diff --git a/packages/SystemUI/res/layout/global_screenshot.xml b/packages/SystemUI/res/layout/global_screenshot.xml index de19303b4948..1dbb38d5dc7a 100644 --- a/packages/SystemUI/res/layout/global_screenshot.xml +++ b/packages/SystemUI/res/layout/global_screenshot.xml @@ -22,7 +22,7 @@ android:layout_height="match_parent"> <ImageView android:id="@+id/global_screenshot_actions_background" - android:layout_height="@dimen/global_screenshot_bg_protection_height" + android:layout_height="@dimen/screenshot_bg_protection_height" android:layout_width="match_parent" android:alpha="0.0" android:src="@drawable/screenshot_actions_background_protection" diff --git a/packages/SystemUI/res/layout/screen_record_dialog.xml b/packages/SystemUI/res/layout/screen_record_dialog.xml index df576d83323f..fd9936f6b8ea 100644 --- a/packages/SystemUI/res/layout/screen_record_dialog.xml +++ b/packages/SystemUI/res/layout/screen_record_dialog.xml @@ -59,29 +59,16 @@ android:layout_gravity="center" android:layout_weight="0" android:layout_marginRight="@dimen/screenrecord_dialog_padding"/> - <LinearLayout + <Spinner + android:id="@+id/screen_recording_options" android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="vertical" - android:layout_weight="1"> - <TextView - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center_vertical" - android:text="@string/screenrecord_audio_label" - android:textColor="?android:attr/textColorPrimary" - android:textAppearance="?android:attr/textAppearanceMedium"/> - <TextView - android:layout_width="match_parent" - android:layout_height="match_parent" - android:id="@+id/audio_type" - android:text="@string/screenrecord_mic_label" - android:textAppearance="?android:attr/textAppearanceSmall"/> - </LinearLayout> + android:layout_height="48dp" + android:prompt="@string/screenrecord_audio_label"/> <Switch android:layout_width="wrap_content" android:layout_height="48dp" - android:layout_weight="0" + android:layout_weight="1" + android:layout_gravity="end" android:id="@+id/screenrecord_audio_switch"/> </LinearLayout> @@ -102,7 +89,8 @@ android:id="@+id/screenrecord_taps_switch" android:text="@string/screenrecord_taps_label" android:textColor="?android:attr/textColorPrimary" - android:textAppearance="?android:attr/textAppearanceMedium"/> + android:textAppearance="?android:attr/textAppearanceSmall"/> + </LinearLayout> </LinearLayout> diff --git a/packages/SystemUI/res/layout/screen_record_dialog_audio_source.xml b/packages/SystemUI/res/layout/screen_record_dialog_audio_source.xml new file mode 100644 index 000000000000..af6f9bb827f6 --- /dev/null +++ b/packages/SystemUI/res/layout/screen_record_dialog_audio_source.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="48dp" + android:orientation="vertical" + android:padding="10dp" + android:layout_weight="1"> + <TextView + android:id="@+id/screen_recording_dialog_source_text" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center_vertical" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorPrimary"/> + <TextView + android:id="@+id/screen_recording_dialog_source_description" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorSecondary"/> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/screen_record_dialog_audio_source_selected.xml b/packages/SystemUI/res/layout/screen_record_dialog_audio_source_selected.xml new file mode 100644 index 000000000000..fabe9e2d4453 --- /dev/null +++ b/packages/SystemUI/res/layout/screen_record_dialog_audio_source_selected.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="48dp" + android:orientation="vertical" + android:layout_weight="1"> + <TextView + android:layout_width="match_parent" + android:layout_height="match_parent" + android:text="@string/screenrecord_audio_label" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorPrimary"/> + <TextView + android:id="@+id/screen_recording_dialog_source_text" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:textColor="?android:attr/textColorSecondary" + android:textAppearance="?android:attr/textAppearanceSmall"/> +</LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml index 2d5101104237..196357c4794e 100644 --- a/packages/SystemUI/res/values-night/colors.xml +++ b/packages/SystemUI/res/values-night/colors.xml @@ -79,6 +79,7 @@ <color name="global_screenshot_button_icon">@color/GM2_blue_300</color> <color name="global_screenshot_dismiss_background">@color/GM2_grey_800</color> <color name="global_screenshot_dismiss_foreground">#FFFFFF</color> + <color name="global_screenshot_background_protection_start">#80000000</color> <!-- 50% black --> <!-- Biometric dialog colors --> diff --git a/packages/SystemUI/res/values-night/dimens.xml b/packages/SystemUI/res/values-night/dimens.xml index 481483991de9..23e323112845 100644 --- a/packages/SystemUI/res/values-night/dimens.xml +++ b/packages/SystemUI/res/values-night/dimens.xml @@ -18,4 +18,8 @@ <resources> <!-- The height of the divider between the individual notifications. --> <dimen name="notification_divider_height">1dp</dimen> + + <!-- Height of the background gradient behind the screenshot UI (taller in dark mode) --> + <dimen name="screenshot_bg_protection_height">375dp</dimen> + </resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 82eda311da6a..b6776005d83e 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -200,6 +200,7 @@ <color name="global_screenshot_button_icon">@color/GM2_blue_500</color> <color name="global_screenshot_dismiss_background">#FFFFFF</color> <color name="global_screenshot_dismiss_foreground">@color/GM2_grey_500</color> + <color name="global_screenshot_background_protection_start">#40000000</color> <!-- 25% black --> <!-- GM2 colors --> <color name="GM2_grey_50">#F8F9FA</color> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 2cbb49801aa8..99e347eb1a69 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -306,6 +306,7 @@ <dimen name="global_screenshot_bg_padding">20dp</dimen> <dimen name="global_screenshot_bg_protection_height">400dp</dimen> <dimen name="global_screenshot_x_scale">80dp</dimen> + <dimen name="screenshot_bg_protection_height">242dp</dimen> <dimen name="screenshot_preview_elevation">6dp</dimen> <dimen name="screenshot_offset_y">48dp</dimen> <dimen name="screenshot_offset_x">16dp</dimen> @@ -1168,7 +1169,7 @@ <!-- Default (and minimum) height of the expanded view shown when the bubble is expanded --> <dimen name="bubble_expanded_default_height">180dp</dimen> <!-- Default height of bubble overflow --> - <dimen name="bubble_overflow_height">460dp</dimen> + <dimen name="bubble_overflow_height">480dp</dimen> <!-- Bubble overflow padding when there are no bubbles --> <dimen name="bubble_overflow_empty_state_padding">16dp</dimen> <!-- Padding of container for overflow bubbles --> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 04640f418e97..8156e8dc9bf1 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -144,5 +144,10 @@ <!-- NotificationPanelView --> <item type="id" name="notification_panel" /> + + <!-- Screen Recording --> + <item type="id" name="screen_recording_options" /> + <item type="id" name="screen_recording_dialog_source_text" /> + <item type="id" name="screen_recording_dialog_source_description" /> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index ee97e7374148..ec29622c9ba2 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -240,6 +240,8 @@ <!-- Notification title displayed for screen recording [CHAR LIMIT=50]--> <string name="screenrecord_name">Screen Recorder</string> + <!-- Processing screen recoding video in the background [CHAR LIMIT=30]--> + <string name="screenrecord_background_processing_label">Processing screen recording</string> <!-- Description of the screen recording notification channel [CHAR LIMIT=NONE]--> <string name="screenrecord_channel_description">Ongoing notification for a screen record session</string> <!-- Title for the screen prompting the user to begin recording their screen [CHAR LIMIT=NONE]--> @@ -2611,6 +2613,10 @@ <!-- Text used for content description of settings button in the header of expanded bubble view. [CHAR_LIMIT=NONE] --> <string name="bubbles_settings_button_description">Settings for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g> bubbles</string> + <!-- Content description for button that shows bubble overflow on click [CHAR LIMIT=NONE] --> + <string name="bubble_overflow_button_content_description">Overflow</string> + <!-- Action to add overflow bubble back to stack. [CHAR LIMIT=NONE] --> + <string name="bubble_accessibility_action_add_back">Add back to stack</string> <!-- The text for the manage bubbles link. [CHAR LIMIT=NONE] --> <string name="manage_bubbles_text">Manage</string> <!-- Content description when a bubble is focused. [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index 7dea7f83f0c6..7c25d2811793 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -432,7 +432,7 @@ public abstract class AuthBiometricView extends LinearLayout { Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); } - public void updateState(@BiometricState int newState) { + void updateState(@BiometricState int newState) { Log.v(TAG, "newState: " + newState); switch (newState) { @@ -453,8 +453,10 @@ public abstract class AuthBiometricView extends LinearLayout { } announceForAccessibility(getResources() .getString(R.string.biometric_dialog_authenticated)); - mHandler.postDelayed(() -> mCallback.onAction(Callback.ACTION_AUTHENTICATED), - getDelayAfterAuthenticatedDurationMs()); + mHandler.postDelayed(() -> { + Log.d(TAG, "Sending ACTION_AUTHENTICATED"); + mCallback.onAction(Callback.ACTION_AUTHENTICATED); + }, getDelayAfterAuthenticatedDurationMs()); break; case STATE_PENDING_CONFIRMATION: diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index b736b4df8abf..86087668604e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -113,6 +113,7 @@ public class AuthContainerView extends LinearLayout int mModalityMask; boolean mSkipIntro; long mOperationId; + int mSysUiSessionId; } public static class Builder { @@ -158,6 +159,11 @@ public class AuthContainerView extends LinearLayout return this; } + public Builder setSysUiSessionId(int sysUiSessionId) { + mConfig.mSysUiSessionId = sysUiSessionId; + return this; + } + public AuthContainerView build(int modalityMask) { mConfig.mModalityMask = modalityMask; return new AuthContainerView(mConfig, new Injector()); @@ -203,6 +209,9 @@ public class AuthContainerView extends LinearLayout final class BiometricCallback implements AuthBiometricView.Callback { @Override public void onAction(int action) { + Log.d(TAG, "onAction: " + action + + ", sysUiSessionId: " + mConfig.mSysUiSessionId + + ", state: " + mContainerState); switch (action) { case AuthBiometricView.Callback.ACTION_AUTHENTICATED: animateAway(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED); @@ -461,13 +470,13 @@ public class AuthContainerView extends LinearLayout if (animate) { animateAway(false /* sendReason */, 0 /* reason */); } else { - removeWindowIfAttached(); + removeWindowIfAttached(false /* sendReason */); } } @Override public void dismissFromSystemServer() { - removeWindowIfAttached(); + removeWindowIfAttached(true /* sendReason */); } @Override @@ -540,7 +549,7 @@ public class AuthContainerView extends LinearLayout final Runnable endActionRunnable = () -> { setVisibility(View.INVISIBLE); - removeWindowIfAttached(); + removeWindowIfAttached(true /* sendReason */); }; postOnAnimation(() -> { @@ -575,19 +584,24 @@ public class AuthContainerView extends LinearLayout } private void sendPendingCallbackIfNotNull() { - Log.d(TAG, "pendingCallback: " + mPendingCallbackReason); + Log.d(TAG, "pendingCallback: " + mPendingCallbackReason + + " sysUISessionId: " + mConfig.mSysUiSessionId); if (mPendingCallbackReason != null) { mConfig.mCallback.onDismissed(mPendingCallbackReason, mCredentialAttestation); mPendingCallbackReason = null; } } - private void removeWindowIfAttached() { - sendPendingCallbackIfNotNull(); + private void removeWindowIfAttached(boolean sendReason) { + if (sendReason) { + sendPendingCallbackIfNotNull(); + } if (mContainerState == STATE_GONE) { + Log.w(TAG, "Container already STATE_GONE, mSysUiSessionId: " + mConfig.mSysUiSessionId); return; } + Log.d(TAG, "Removing container, mSysUiSessionId: " + mConfig.mSysUiSessionId); mContainerState = STATE_GONE; mWindowManager.removeView(this); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 0c6794c2ab85..9f0ea3ee46ff 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -276,14 +276,15 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, @Override public void showAuthenticationDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, int biometricModality, boolean requireConfirmation, int userId, String opPackageName, - long operationId) { + long operationId, int sysUiSessionId) { final int authenticators = Utils.getAuthenticators(bundle); if (DEBUG) { Log.d(TAG, "showAuthenticationDialog, authenticators: " + authenticators + ", biometricModality: " + biometricModality + ", requireConfirmation: " + requireConfirmation - + ", operationId: " + operationId); + + ", operationId: " + operationId + + ", sysUiSessionId: " + sysUiSessionId); } SomeArgs args = SomeArgs.obtain(); args.arg1 = bundle; @@ -293,6 +294,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, args.argi2 = userId; args.arg4 = opPackageName; args.arg5 = operationId; + args.argi3 = sysUiSessionId; boolean skipAnimation = false; if (mCurrentDialog != null) { @@ -382,6 +384,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, final int userId = args.argi2; final String opPackageName = (String) args.arg4; final long operationId = (long) args.arg5; + final int sysUiSessionId = args.argi3; // Create a new dialog but do not replace the current one yet. final AuthDialog newDialog = buildDialog( @@ -391,7 +394,8 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, type, opPackageName, skipAnimation, - operationId); + operationId, + sysUiSessionId); if (newDialog == null) { Log.e(TAG, "Unsupported type: " + type); @@ -403,7 +407,8 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, + " savedState: " + savedState + " mCurrentDialog: " + mCurrentDialog + " newDialog: " + newDialog - + " type: " + type); + + " type: " + type + + " sysUiSessionId: " + sysUiSessionId); } if (mCurrentDialog != null) { @@ -458,7 +463,8 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, } protected AuthDialog buildDialog(Bundle biometricPromptBundle, boolean requireConfirmation, - int userId, int type, String opPackageName, boolean skipIntro, long operationId) { + int userId, int type, String opPackageName, boolean skipIntro, long operationId, + int sysUiSessionId) { return new AuthContainerView.Builder(mContext) .setCallback(this) .setBiometricPromptBundle(biometricPromptBundle) @@ -467,6 +473,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks, .setOpPackageName(opPackageName) .setSkipIntro(skipIntro) .setOperationId(operationId) + .setSysUiSessionId(sysUiSessionId) .build(type); } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.java index 13669a68defa..e96bef36ba18 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflow.java @@ -73,12 +73,13 @@ public class BubbleOverflow implements BubbleViewProvider { updateIcon(mContext, parentViewGroup); } - // TODO(b/149146374) Propagate theme change to bubbles in overflow. void updateIcon(Context context, ViewGroup parentViewGroup) { mInflater = LayoutInflater.from(context); mOverflowBtn = (BadgedImageView) mInflater.inflate(R.layout.bubble_overflow_button, parentViewGroup /* root */, false /* attachToRoot */); + mOverflowBtn.setContentDescription(mContext.getResources().getString( + R.string.bubble_overflow_button_content_description)); TypedArray ta = mContext.obtainStyledAttributes( new int[]{android.R.attr.colorBackgroundFloating}); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java index de54c353fc85..c2ca9fad6d43 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleOverflowActivity.java @@ -21,6 +21,7 @@ import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; import android.app.Activity; +import android.app.Notification; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -32,6 +33,7 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; @@ -103,7 +105,7 @@ public class BubbleOverflowActivity extends Activity { - res.getDimensionPixelSize(R.dimen.bubble_overflow_padding); final int viewHeight = recyclerViewHeight / rows; - mAdapter = new BubbleOverflowAdapter(mOverflowBubbles, + mAdapter = new BubbleOverflowAdapter(getApplicationContext(), mOverflowBubbles, mBubbleController::promoteBubbleFromOverflow, viewWidth, viewHeight); mRecyclerView.setAdapter(mAdapter); @@ -221,13 +223,15 @@ public class BubbleOverflowActivity extends Activity { } class BubbleOverflowAdapter extends RecyclerView.Adapter<BubbleOverflowAdapter.ViewHolder> { + private Context mContext; private Consumer<Bubble> mPromoteBubbleFromOverflow; private List<Bubble> mBubbles; private int mWidth; private int mHeight; - public BubbleOverflowAdapter(List<Bubble> list, Consumer<Bubble> promoteBubble, int width, - int height) { + public BubbleOverflowAdapter(Context context, List<Bubble> list, Consumer<Bubble> promoteBubble, + int width, int height) { + mContext = context; mBubbles = list; mPromoteBubbleFromOverflow = promoteBubble; mWidth = width; @@ -260,6 +264,32 @@ class BubbleOverflowAdapter extends RecyclerView.Adapter<BubbleOverflowAdapter.V mPromoteBubbleFromOverflow.accept(b); }); + final CharSequence titleCharSeq = + b.getEntry().getSbn().getNotification().extras.getCharSequence( + Notification.EXTRA_TITLE); + String titleStr = mContext.getResources().getString(R.string.notification_bubble_title); + if (titleCharSeq != null) { + titleStr = titleCharSeq.toString(); + } + vh.iconView.setContentDescription(mContext.getResources().getString( + R.string.bubble_content_description_single, titleStr, b.getAppName())); + + vh.iconView.setAccessibilityDelegate( + new View.AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, + AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + // Talkback prompts "Double tap to add back to stack" + // instead of the default "Double tap to activate" + info.addAction( + new AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLICK, + mContext.getResources().getString( + R.string.bubble_accessibility_action_add_back))); + } + }); + Bubble.FlyoutMessage message = b.getFlyoutMessage(); if (message != null && message.senderName != null) { vh.textView.setText(message.senderName); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index 341458763625..2cb097f6075e 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -454,7 +454,6 @@ public class BubbleStackView extends FrameLayout // that means overflow was previously expanded. Set the selected bubble // internally without going through BubbleData (which would ignore it since it's // already selected). - mBubbleData.setShowingOverflow(true); setSelectedBubble(clickedBubble); } } else { @@ -1344,7 +1343,10 @@ public class BubbleStackView extends FrameLayout } if (bubbleToSelect == null || bubbleToSelect.getKey() != BubbleOverflow.KEY) { mBubbleData.setShowingOverflow(false); + } else { + mBubbleData.setShowingOverflow(true); } + final BubbleViewProvider previouslySelected = mExpandedBubble; mExpandedBubble = bubbleToSelect; updatePointerPosition(); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/LongRunning.java b/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/LongRunning.java new file mode 100644 index 000000000000..e90781b48f23 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dagger/qualifiers/LongRunning.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dagger.qualifiers; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import javax.inject.Qualifier; + +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface LongRunning { +} diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java index f322489b8dc2..f72de11a01ed 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java @@ -376,7 +376,8 @@ public class PipAnimationController { // NOTE: intentionally does not apply the transaction here. // this end transaction should get executed synchronously with the final // WindowContainerTransaction in task organizer - getSurfaceTransactionHelper().resetScale(tx, leash, getDestinationBounds()); + getSurfaceTransactionHelper().resetScale(tx, leash, getDestinationBounds()) + .crop(tx, leash, getDestinationBounds()); } }; } diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java index 8d6ce4718aef..7e2efc04ea8e 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java @@ -552,6 +552,9 @@ public class PipTaskOrganizer extends TaskOrganizer { ? null : destinationBounds; // As for the final windowing mode, simply reset it to undefined. wct.setWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); + if (mSplitDivider != null && direction == TRANSITION_DIRECTION_TO_SPLIT_SCREEN) { + wct.reparent(mToken, mSplitDivider.getSecondaryRoot(), true /* onTop */); + } } else { taskBounds = destinationBounds; } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java index ae0a1c4d9822..b253635e9bfa 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java @@ -137,7 +137,7 @@ public class RecordingController * Check if the recording is ongoing * @return */ - public boolean isRecording() { + public synchronized boolean isRecording() { return mIsRecording; } @@ -157,7 +157,7 @@ public class RecordingController * Update the current status * @param isRecording */ - public void updateState(boolean isRecording) { + public synchronized void updateState(boolean isRecording) { mIsRecording = isRecording; for (RecordingStateChangeCallback cb : mListeners) { if (isRecording) { diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index 390ac0969b21..cf098d52fa91 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -22,41 +22,27 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.ContentResolver; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.Icon; -import android.hardware.display.DisplayManager; -import android.hardware.display.VirtualDisplay; import android.media.MediaRecorder; -import android.media.projection.IMediaProjection; -import android.media.projection.IMediaProjectionManager; -import android.media.projection.MediaProjection; -import android.media.projection.MediaProjectionManager; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; -import android.os.ServiceManager; -import android.provider.MediaStore; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Log; import android.util.Size; -import android.view.Surface; -import android.view.WindowManager; import android.widget.Toast; import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.LongRunning; -import java.io.File; import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.util.concurrent.Executor; import javax.inject.Inject; @@ -66,13 +52,15 @@ import javax.inject.Inject; public class RecordingService extends Service implements MediaRecorder.OnInfoListener { public static final int REQUEST_CODE = 2; - private static final int NOTIFICATION_ID = 1; + private static final int NOTIFICATION_RECORDING_ID = 4274; + private static final int NOTIFICATION_PROCESSING_ID = 4275; + private static final int NOTIFICATION_VIEW_ID = 4273; private static final String TAG = "RecordingService"; private static final String CHANNEL_ID = "screen_record"; private static final String EXTRA_RESULT_CODE = "extra_resultCode"; private static final String EXTRA_DATA = "extra_data"; private static final String EXTRA_PATH = "extra_path"; - private static final String EXTRA_USE_AUDIO = "extra_useAudio"; + private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio"; private static final String EXTRA_SHOW_TAPS = "extra_showTaps"; private static final String ACTION_START = "com.android.systemui.screenrecord.START"; @@ -80,29 +68,19 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis private static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE"; private static final String ACTION_DELETE = "com.android.systemui.screenrecord.DELETE"; - private static final int TOTAL_NUM_TRACKS = 1; - private static final int VIDEO_BIT_RATE = 10000000; - private static final int VIDEO_FRAME_RATE = 30; - private static final int AUDIO_BIT_RATE = 16; - private static final int AUDIO_SAMPLE_RATE = 44100; - private static final int MAX_DURATION_MS = 60 * 60 * 1000; - private static final long MAX_FILESIZE_BYTES = 5000000000L; - private final RecordingController mController; - private MediaProjection mMediaProjection; - private Surface mInputSurface; - private VirtualDisplay mVirtualDisplay; - private MediaRecorder mMediaRecorder; private Notification.Builder mRecordingNotificationBuilder; - private boolean mUseAudio; + private ScreenRecordingAudioSource mAudioSource; private boolean mShowTaps; private boolean mOriginalShowTaps; - private File mTempFile; + private ScreenMediaRecorder mRecorder; + private final Executor mLongExecutor; @Inject - public RecordingService(RecordingController controller) { + public RecordingService(RecordingController controller, @LongRunning Executor executor) { mController = controller; + mLongExecutor = executor; } /** @@ -113,16 +91,16 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis * android.content.Intent)} * @param data The data from {@link android.app.Activity#onActivityResult(int, int, * android.content.Intent)} - * @param useAudio True to enable microphone input while recording + * @param audioSource The ordinal value of the audio source + * {@link com.android.systemui.screenrecord.ScreenRecordingAudioSource} * @param showTaps True to make touches visible while recording */ - public static Intent getStartIntent(Context context, int resultCode, Intent data, - boolean useAudio, boolean showTaps) { + public static Intent getStartIntent(Context context, int resultCode, + int audioSource, boolean showTaps) { return new Intent(context, RecordingService.class) .setAction(ACTION_START) .putExtra(EXTRA_RESULT_CODE, resultCode) - .putExtra(EXTRA_DATA, data) - .putExtra(EXTRA_USE_AUDIO, useAudio) + .putExtra(EXTRA_AUDIO_SOURCE, audioSource) .putExtra(EXTRA_SHOW_TAPS, showTaps); } @@ -139,36 +117,31 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis switch (action) { case ACTION_START: - mUseAudio = intent.getBooleanExtra(EXTRA_USE_AUDIO, false); + mAudioSource = ScreenRecordingAudioSource + .values()[intent.getIntExtra(EXTRA_AUDIO_SOURCE, 0)]; + Log.d(TAG, "recording with audio source" + mAudioSource); mShowTaps = intent.getBooleanExtra(EXTRA_SHOW_TAPS, false); - try { - IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE); - IMediaProjectionManager mediaService = - IMediaProjectionManager.Stub.asInterface(b); - IMediaProjection proj = mediaService.createProjection(getUserId(), - getPackageName(), - MediaProjectionManager.TYPE_SCREEN_CAPTURE, false); - IBinder projection = proj.asBinder(); - if (projection == null) { - Log.e(TAG, "Projection was null"); - Toast.makeText(this, R.string.screenrecord_start_error, Toast.LENGTH_LONG) - .show(); - return Service.START_NOT_STICKY; - } - mMediaProjection = new MediaProjection(getApplicationContext(), - IMediaProjection.Stub.asInterface(projection)); - startRecording(); - } catch (RemoteException e) { - e.printStackTrace(); - Toast.makeText(this, R.string.screenrecord_start_error, Toast.LENGTH_LONG) - .show(); - return Service.START_NOT_STICKY; - } + + mOriginalShowTaps = Settings.System.getInt( + getApplicationContext().getContentResolver(), + Settings.System.SHOW_TOUCHES, 0) != 0; + + setTapsVisible(mShowTaps); + + mRecorder = new ScreenMediaRecorder( + getApplicationContext(), + getUserId(), + mAudioSource, + this + ); + startRecording(); break; case ACTION_STOP: stopRecording(); + notificationManager.cancel(NOTIFICATION_RECORDING_ID); saveRecording(notificationManager); + stopSelf(); break; case ACTION_SHARE: @@ -183,10 +156,10 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); // Remove notification - notificationManager.cancel(NOTIFICATION_ID); + notificationManager.cancel(NOTIFICATION_RECORDING_ID); startActivity(Intent.createChooser(shareIntent, shareLabel) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); break; case ACTION_DELETE: // Close quick shade @@ -202,7 +175,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis Toast.LENGTH_LONG).show(); // Remove notification - notificationManager.cancel(NOTIFICATION_ID); + notificationManager.cancel(NOTIFICATION_RECORDING_ID); Log.d(TAG, "Deleted recording " + uri); break; } @@ -224,70 +197,15 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis */ private void startRecording() { try { - File cacheDir = getCacheDir(); - cacheDir.mkdirs(); - mTempFile = File.createTempFile("temp", ".mp4", cacheDir); - Log.d(TAG, "Writing video output to: " + mTempFile.getAbsolutePath()); - - mOriginalShowTaps = 1 == Settings.System.getInt( - getApplicationContext().getContentResolver(), - Settings.System.SHOW_TOUCHES, 0); - setTapsVisible(mShowTaps); - - // Set up media recorder - mMediaRecorder = new MediaRecorder(); - if (mUseAudio) { - mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); - } - mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); - mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); - - // Set up video - DisplayMetrics metrics = new DisplayMetrics(); - WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); - wm.getDefaultDisplay().getRealMetrics(metrics); - int screenWidth = metrics.widthPixels; - int screenHeight = metrics.heightPixels; - mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); - mMediaRecorder.setVideoSize(screenWidth, screenHeight); - mMediaRecorder.setVideoFrameRate(VIDEO_FRAME_RATE); - mMediaRecorder.setVideoEncodingBitRate(VIDEO_BIT_RATE); - mMediaRecorder.setMaxDuration(MAX_DURATION_MS); - mMediaRecorder.setMaxFileSize(MAX_FILESIZE_BYTES); - - // Set up audio - if (mUseAudio) { - mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); - mMediaRecorder.setAudioChannels(TOTAL_NUM_TRACKS); - mMediaRecorder.setAudioEncodingBitRate(AUDIO_BIT_RATE); - mMediaRecorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE); - } - - mMediaRecorder.setOutputFile(mTempFile); - mMediaRecorder.prepare(); - - // Create surface - mInputSurface = mMediaRecorder.getSurface(); - mVirtualDisplay = mMediaProjection.createVirtualDisplay( - "Recording Display", - screenWidth, - screenHeight, - metrics.densityDpi, - DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, - mInputSurface, - null, - null); - - mMediaRecorder.setOnInfoListener(this); - mMediaRecorder.start(); + mRecorder.start(); mController.updateState(true); - } catch (IOException e) { - Log.e(TAG, "Error starting screen recording: " + e.getMessage()); + createRecordingNotification(); + } catch (IOException | RemoteException e) { + Toast.makeText(this, + R.string.screenrecord_start_error, Toast.LENGTH_LONG) + .show(); e.printStackTrace(); - throw new RuntimeException(e); } - - createRecordingNotification(); } private void createRecordingNotification() { @@ -306,7 +224,7 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, res.getString(R.string.screenrecord_name)); - String notificationTitle = mUseAudio + String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE ? res.getString(R.string.screenrecord_ongoing_screen_and_audio) : res.getString(R.string.screenrecord_ongoing_screen_only); @@ -323,9 +241,10 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis this, REQUEST_CODE, getStopIntent(this), PendingIntent.FLAG_UPDATE_CURRENT)) .addExtras(extras); - notificationManager.notify(NOTIFICATION_ID, mRecordingNotificationBuilder.build()); + notificationManager.notify(NOTIFICATION_RECORDING_ID, + mRecordingNotificationBuilder.build()); Notification notification = mRecordingNotificationBuilder.build(); - startForeground(NOTIFICATION_ID, notification); + startForeground(NOTIFICATION_RECORDING_ID, notification); } private Notification createSaveNotification(Uri uri) { @@ -392,47 +311,38 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis private void stopRecording() { setTapsVisible(mOriginalShowTaps); - mMediaRecorder.stop(); - mMediaRecorder.release(); - mMediaRecorder = null; - mMediaProjection.stop(); - mMediaProjection = null; - mInputSurface.release(); - mVirtualDisplay.release(); - stopSelf(); + mRecorder.end(); mController.updateState(false); } private void saveRecording(NotificationManager notificationManager) { - String fileName = new SimpleDateFormat("'screen-'yyyyMMdd-HHmmss'.mp4'") - .format(new Date()); - - ContentValues values = new ContentValues(); - values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName); - values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4"); - values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis()); - values.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis()); - - ContentResolver resolver = getContentResolver(); - Uri collectionUri = MediaStore.Video.Media.getContentUri( - MediaStore.VOLUME_EXTERNAL_PRIMARY); - Uri itemUri = resolver.insert(collectionUri, values); - - try { - // Add to the mediastore - OutputStream os = resolver.openOutputStream(itemUri, "w"); - Files.copy(mTempFile.toPath(), os); - os.close(); - - Notification notification = createSaveNotification(itemUri); - notificationManager.notify(NOTIFICATION_ID, notification); - - mTempFile.delete(); - } catch (IOException e) { - Log.e(TAG, "Error saving screen recording: " + e.getMessage()); - Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG) - .show(); - } + Resources res = getApplicationContext().getResources(); + String notificationTitle = mAudioSource == ScreenRecordingAudioSource.NONE + ? res.getString(R.string.screenrecord_ongoing_screen_only) + : res.getString(R.string.screenrecord_ongoing_screen_and_audio); + Notification.Builder builder = new Notification.Builder(getApplicationContext(), CHANNEL_ID) + .setContentTitle(notificationTitle) + .setContentText( + getResources().getString(R.string.screenrecord_background_processing_label)) + .setSmallIcon(R.drawable.ic_screenrecord); + notificationManager.notify(NOTIFICATION_PROCESSING_ID, builder.build()); + + mLongExecutor.execute(() -> { + try { + Log.d(TAG, "saving recording"); + Notification notification = createSaveNotification(mRecorder.save()); + if (!mController.isRecording()) { + Log.d(TAG, "showing saved notification"); + notificationManager.notify(NOTIFICATION_VIEW_ID, notification); + } + } catch (IOException e) { + Log.e(TAG, "Error saving screen recording: " + e.getMessage()); + Toast.makeText(this, R.string.screenrecord_delete_error, Toast.LENGTH_LONG) + .show(); + } finally { + notificationManager.cancel(NOTIFICATION_PROCESSING_ID); + } + }); } private void setTapsVisible(boolean turnOn) { diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java new file mode 100644 index 000000000000..752f4fddf24b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenInternalAudioRecorder.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenrecord; + +import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioFormat; +import android.media.AudioPlaybackCaptureConfiguration; +import android.media.AudioRecord; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.media.MediaMuxer; +import android.media.MediaRecorder; +import android.media.projection.MediaProjection; +import android.util.Log; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Recording internal audio + */ +public class ScreenInternalAudioRecorder { + private static String TAG = "ScreenAudioRecorder"; + private static final int TIMEOUT = 500; + private final Context mContext; + private AudioRecord mAudioRecord; + private AudioRecord mAudioRecordMic; + private Config mConfig = new Config(); + private Thread mThread; + private MediaProjection mMediaProjection; + private MediaCodec mCodec; + private long mPresentationTime; + private long mTotalBytes; + private MediaMuxer mMuxer; + private String mOutFile; + private boolean mMic; + + private int mTrackId = -1; + + public ScreenInternalAudioRecorder(String outFile, Context context, + MediaProjection mp, boolean includeMicInput) throws IOException { + mMic = includeMicInput; + mOutFile = outFile; + mMuxer = new MediaMuxer(outFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + mContext = context; + mMediaProjection = mp; + Log.d(TAG, "creating audio file " + outFile); + setupSimple(); + } + /** + * Audio recoding configuration + */ + public static class Config { + public int channelOutMask = AudioFormat.CHANNEL_OUT_MONO; + public int channelInMask = AudioFormat.CHANNEL_IN_MONO; + public int encoding = AudioFormat.ENCODING_PCM_16BIT; + public int sampleRate = 44100; + public int bitRate = 196000; + public int bufferSizeBytes = 1 << 17; + public boolean privileged = true; + public boolean legacy_app_looback = false; + + @Override + public String toString() { + return "channelMask=" + channelOutMask + + "\n encoding=" + encoding + + "\n sampleRate=" + sampleRate + + "\n bufferSize=" + bufferSizeBytes + + "\n privileged=" + privileged + + "\n legacy app looback=" + legacy_app_looback; + } + + } + + private void setupSimple() throws IOException { + int size = AudioRecord.getMinBufferSize( + mConfig.sampleRate, mConfig.channelInMask, + mConfig.encoding) * 2; + + Log.d(TAG, "audio buffer size: " + size); + + AudioFormat format = new AudioFormat.Builder() + .setEncoding(mConfig.encoding) + .setSampleRate(mConfig.sampleRate) + .setChannelMask(mConfig.channelOutMask) + .build(); + + AudioPlaybackCaptureConfiguration playbackConfig = + new AudioPlaybackCaptureConfiguration.Builder(mMediaProjection) + .addMatchingUsage(AudioAttributes.USAGE_MEDIA) + .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN) + .addMatchingUsage(AudioAttributes.USAGE_GAME) + .build(); + + mAudioRecord = new AudioRecord.Builder() + .setAudioFormat(format) + .setAudioPlaybackCaptureConfig(playbackConfig) + .build(); + + if (mMic) { + mAudioRecordMic = new AudioRecord(MediaRecorder.AudioSource.VOICE_COMMUNICATION, + mConfig.sampleRate, AudioFormat.CHANNEL_IN_MONO, mConfig.encoding, size); + } + + mCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC); + MediaFormat medFormat = MediaFormat.createAudioFormat( + MediaFormat.MIMETYPE_AUDIO_AAC, mConfig.sampleRate, 1); + medFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, + MediaCodecInfo.CodecProfileLevel.AACObjectLC); + medFormat.setInteger(MediaFormat.KEY_BIT_RATE, mConfig.bitRate); + medFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, mConfig.encoding); + mCodec.configure(medFormat, + null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + + mThread = new Thread(() -> { + short[] bufferInternal = null; + short[] bufferMic = null; + byte[] buffer = null; + + if (mMic) { + bufferInternal = new short[size / 2]; + bufferMic = new short[size / 2]; + } else { + buffer = new byte[size]; + } + + while (true) { + int readBytes = 0; + int readShortsInternal = 0; + int readShortsMic = 0; + if (mMic) { + readShortsInternal = mAudioRecord.read(bufferInternal, 0, + bufferInternal.length); + readShortsMic = mAudioRecordMic.read(bufferMic, 0, bufferMic.length); + readBytes = Math.min(readShortsInternal, readShortsMic) * 2; + buffer = addAndConvertBuffers(bufferInternal, readShortsInternal, bufferMic, + readShortsMic); + } else { + readBytes = mAudioRecord.read(buffer, 0, buffer.length); + } + + //exit the loop when at end of stream + if (readBytes < 0) { + Log.e(TAG, "read error " + readBytes + + ", shorts internal: " + readShortsInternal + + ", shorts mic: " + readShortsMic); + break; + } + encode(buffer, readBytes); + } + endStream(); + }); + } + + private byte[] addAndConvertBuffers(short[] a1, int a1Limit, short[] a2, int a2Limit) { + int size = Math.max(a1Limit, a2Limit); + if (size < 0) return new byte[0]; + byte[] buff = new byte[size * 2]; + for (int i = 0; i < size; i++) { + int sum; + if (i > a1Limit) { + sum = a2[i]; + } else if (i > a2Limit) { + sum = a1[i]; + } else { + sum = (int) a1[i] + (int) a2[i]; + } + + if (sum > Short.MAX_VALUE) sum = Short.MAX_VALUE; + if (sum < Short.MIN_VALUE) sum = Short.MIN_VALUE; + int byteIndex = i * 2; + buff[byteIndex] = (byte) (sum & 0xff); + buff[byteIndex + 1] = (byte) ((sum >> 8) & 0xff); + } + return buff; + } + + private void encode(byte[] buffer, int readBytes) { + int offset = 0; + while (readBytes > 0) { + int totalBytesRead = 0; + int bufferIndex = mCodec.dequeueInputBuffer(TIMEOUT); + if (bufferIndex < 0) { + writeOutput(); + return; + } + ByteBuffer buff = mCodec.getInputBuffer(bufferIndex); + buff.clear(); + int bufferSize = buff.capacity(); + int bytesToRead = readBytes > bufferSize ? bufferSize : readBytes; + totalBytesRead += bytesToRead; + readBytes -= bytesToRead; + buff.put(buffer, offset, bytesToRead); + offset += bytesToRead; + mCodec.queueInputBuffer(bufferIndex, 0, bytesToRead, mPresentationTime, 0); + mTotalBytes += totalBytesRead; + mPresentationTime = 1000000L * (mTotalBytes / 2) / mConfig.sampleRate; + + writeOutput(); + } + } + + private void endStream() { + int bufferIndex = mCodec.dequeueInputBuffer(TIMEOUT); + mCodec.queueInputBuffer(bufferIndex, 0, 0, mPresentationTime, + MediaCodec.BUFFER_FLAG_END_OF_STREAM); + writeOutput(); + } + + private void writeOutput() { + while (true) { + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + int bufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT); + if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + mTrackId = mMuxer.addTrack(mCodec.getOutputFormat()); + mMuxer.start(); + continue; + } + if (bufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { + break; + } + if (mTrackId < 0) return; + ByteBuffer buff = mCodec.getOutputBuffer(bufferIndex); + + if (!((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0 + && bufferInfo.size != 0)) { + mMuxer.writeSampleData(mTrackId, buff, bufferInfo); + } + mCodec.releaseOutputBuffer(bufferIndex, false); + } + } + + /** + * start recording + */ + public void start() { + if (mThread != null) { + Log.e(TAG, "a recording is being done in parallel or stop is not called"); + } + mAudioRecord.startRecording(); + if (mMic) mAudioRecordMic.startRecording(); + Log.d(TAG, "channel count " + mAudioRecord.getChannelCount()); + mCodec.start(); + if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) { + Log.e(TAG, "Error starting audio recording"); + return; + } + mThread.start(); + } + + /** + * end recording + */ + public void end() { + mAudioRecord.stop(); + if (mMic) { + mAudioRecordMic.stop(); + } + mAudioRecord.release(); + if (mMic) { + mAudioRecordMic.release(); + } + try { + mThread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + mCodec.stop(); + mCodec.release(); + mMuxer.stop(); + mMuxer.release(); + mThread = null; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java new file mode 100644 index 000000000000..c967648c544e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenrecord; + +import static android.content.Context.MEDIA_PROJECTION_SERVICE; + +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.INTERNAL; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.media.MediaMuxer; +import android.media.MediaRecorder; +import android.media.projection.IMediaProjection; +import android.media.projection.IMediaProjectionManager; +import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; +import android.net.Uri; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.provider.MediaStore; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Surface; +import android.view.WindowManager; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * Recording screen and mic/internal audio + */ +public class ScreenMediaRecorder { + private static final int TOTAL_NUM_TRACKS = 1; + private static final int VIDEO_BIT_RATE = 10000000; + private static final int VIDEO_FRAME_RATE = 30; + private static final int AUDIO_BIT_RATE = 16; + private static final int AUDIO_SAMPLE_RATE = 44100; + private static final int MAX_DURATION_MS = 60 * 60 * 1000; + private static final long MAX_FILESIZE_BYTES = 5000000000L; + private static final String TAG = "ScreenMediaRecorder"; + + + private File mTempVideoFile; + private File mTempAudioFile; + private MediaProjection mMediaProjection; + private Surface mInputSurface; + private VirtualDisplay mVirtualDisplay; + private MediaRecorder mMediaRecorder; + private int mUser; + private ScreenRecordingMuxer mMuxer; + private ScreenInternalAudioRecorder mAudio; + private ScreenRecordingAudioSource mAudioSource; + + private Context mContext; + MediaRecorder.OnInfoListener mListener; + + public ScreenMediaRecorder(Context context, + int user, ScreenRecordingAudioSource audioSource, + MediaRecorder.OnInfoListener listener) { + mContext = context; + mUser = user; + mListener = listener; + mAudioSource = audioSource; + } + + private void prepare() throws IOException, RemoteException { + //Setup media projection + IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE); + IMediaProjectionManager mediaService = + IMediaProjectionManager.Stub.asInterface(b); + IMediaProjection proj = null; + proj = mediaService.createProjection(mUser, mContext.getPackageName(), + MediaProjectionManager.TYPE_SCREEN_CAPTURE, false); + IBinder projection = proj.asBinder(); + mMediaProjection = new MediaProjection(mContext, + IMediaProjection.Stub.asInterface(projection)); + + File cacheDir = mContext.getCacheDir(); + cacheDir.mkdirs(); + mTempVideoFile = File.createTempFile("temp", ".mp4", cacheDir); + + // Set up media recorder + mMediaRecorder = new MediaRecorder(); + + // Set up audio source + if (mAudioSource == MIC) { + mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); + } + mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); + + mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); + + + // Set up video + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + wm.getDefaultDisplay().getRealMetrics(metrics); + int screenWidth = metrics.widthPixels; + int screenHeight = metrics.heightPixels; + mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); + mMediaRecorder.setVideoSize(screenWidth, screenHeight); + mMediaRecorder.setVideoFrameRate(VIDEO_FRAME_RATE); + mMediaRecorder.setVideoEncodingBitRate(VIDEO_BIT_RATE); + mMediaRecorder.setMaxDuration(MAX_DURATION_MS); + mMediaRecorder.setMaxFileSize(MAX_FILESIZE_BYTES); + + // Set up audio + if (mAudioSource == MIC) { + mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC); + mMediaRecorder.setAudioChannels(TOTAL_NUM_TRACKS); + mMediaRecorder.setAudioEncodingBitRate(AUDIO_BIT_RATE); + mMediaRecorder.setAudioSamplingRate(AUDIO_SAMPLE_RATE); + } + + mMediaRecorder.setOutputFile(mTempVideoFile); + mMediaRecorder.prepare(); + // Create surface + mInputSurface = mMediaRecorder.getSurface(); + mVirtualDisplay = mMediaProjection.createVirtualDisplay( + "Recording Display", + screenWidth, + screenHeight, + metrics.densityDpi, + DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, + mInputSurface, + null, + null); + + mMediaRecorder.setOnInfoListener(mListener); + if (mAudioSource == INTERNAL || + mAudioSource == MIC_AND_INTERNAL) { + mTempAudioFile = File.createTempFile("temp", ".aac", + mContext.getCacheDir()); + mAudio = new ScreenInternalAudioRecorder(mTempAudioFile.getAbsolutePath(), mContext, + mMediaProjection, mAudioSource == MIC_AND_INTERNAL); + } + + } + + /** + * Start screen recording + */ + void start() throws IOException, RemoteException { + Log.d(TAG, "start recording"); + prepare(); + mMediaRecorder.start(); + recordInternalAudio(); + } + + /** + * End screen recording + */ + void end() { + mMediaRecorder.stop(); + mMediaProjection.stop(); + mMediaRecorder.release(); + mMediaRecorder = null; + mMediaProjection = null; + mInputSurface.release(); + mVirtualDisplay.release(); + stopInternalAudioRecording(); + + Log.d(TAG, "end recording"); + } + + private void stopInternalAudioRecording() { + if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) { + mAudio.end(); + mAudio = null; + } + } + + private void recordInternalAudio() { + if (mAudioSource == INTERNAL || mAudioSource == MIC_AND_INTERNAL) { + mAudio.start(); + } + } + + /** + * Store recorded video + */ + Uri save() throws IOException { + String fileName = new SimpleDateFormat("'screen-'yyyyMMdd-HHmmss'.mp4'") + .format(new Date()); + + ContentValues values = new ContentValues(); + values.put(MediaStore.Video.Media.DISPLAY_NAME, fileName); + values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4"); + values.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis()); + values.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis()); + + ContentResolver resolver = mContext.getContentResolver(); + Uri collectionUri = MediaStore.Video.Media.getContentUri( + MediaStore.VOLUME_EXTERNAL_PRIMARY); + Uri itemUri = resolver.insert(collectionUri, values); + + Log.d(TAG, itemUri.toString()); + if (mAudioSource == MIC_AND_INTERNAL || mAudioSource == INTERNAL) { + try { + Log.d(TAG, "muxing recording"); + File file = File.createTempFile("temp", ".mp4", + mContext.getCacheDir()); + mMuxer = new ScreenRecordingMuxer(MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, + file.getAbsolutePath(), + mTempVideoFile.getAbsolutePath(), + mTempAudioFile.getAbsolutePath()); + mMuxer.mux(); + mTempVideoFile.delete(); + mTempVideoFile = file; + } catch (IOException e) { + Log.e(TAG, "muxing recording " + e.getMessage()); + e.printStackTrace(); + } + } + + // Add to the mediastore + OutputStream os = resolver.openOutputStream(itemUri, "w"); + Files.copy(mTempVideoFile.toPath(), os); + os.close(); + mTempVideoFile.delete(); + if (mTempAudioFile != null) mTempAudioFile.delete(); + return itemUri; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java index 26973d092209..c247328078a7 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordDialog.java @@ -16,17 +16,30 @@ package com.android.systemui.screenrecord; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.INTERNAL; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.NONE; + import android.app.Activity; import android.app.PendingIntent; import android.os.Bundle; +import android.util.Log; import android.view.Gravity; +import android.view.View; import android.view.ViewGroup; import android.view.Window; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; import android.widget.Button; +import android.widget.Spinner; import android.widget.Switch; import com.android.systemui.R; +import java.util.ArrayList; +import java.util.List; + import javax.inject.Inject; /** @@ -35,10 +48,15 @@ import javax.inject.Inject; public class ScreenRecordDialog extends Activity { private static final long DELAY_MS = 3000; private static final long INTERVAL_MS = 1000; + private static final String TAG = "ScreenRecordDialog"; private final RecordingController mController; - private Switch mAudioSwitch; private Switch mTapsSwitch; + private Switch mAudioSwitch; + private Spinner mOptions; + private List<ScreenRecordingAudioSource> mModes; + private int mSelected; + @Inject public ScreenRecordDialog(RecordingController controller) { @@ -68,17 +86,32 @@ public class ScreenRecordDialog extends Activity { finish(); }); + mModes = new ArrayList<>(); + mModes.add(INTERNAL); + mModes.add(MIC); + mModes.add(MIC_AND_INTERNAL); + mAudioSwitch = findViewById(R.id.screenrecord_audio_switch); mTapsSwitch = findViewById(R.id.screenrecord_taps_switch); + mOptions = findViewById(R.id.screen_recording_options); + ArrayAdapter a = new ScreenRecordingAdapter(getApplicationContext(), + android.R.layout.simple_spinner_dropdown_item, + mModes); + a.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mOptions.setAdapter(a); + } private void requestScreenCapture() { - boolean useAudio = mAudioSwitch.isChecked(); boolean showTaps = mTapsSwitch.isChecked(); + ScreenRecordingAudioSource audioMode = mAudioSwitch.isChecked() + ? (ScreenRecordingAudioSource) mOptions.getSelectedItem() + : NONE; PendingIntent startIntent = PendingIntent.getForegroundService(this, RecordingService.REQUEST_CODE, RecordingService.getStartIntent( - ScreenRecordDialog.this, RESULT_OK, null, useAudio, showTaps), + ScreenRecordDialog.this, RESULT_OK, + audioMode.ordinal(), showTaps), PendingIntent.FLAG_UPDATE_CURRENT); PendingIntent stopIntent = PendingIntent.getService(this, RecordingService.REQUEST_CODE, diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAdapter.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAdapter.java new file mode 100644 index 000000000000..2e0e746594b4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAdapter.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenrecord; + +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.INTERNAL; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC; +import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL; + +import android.content.Context; +import android.content.res.Resources; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.systemui.R; + +import java.util.List; + +/** + * Screen recording view adapter + */ +public class ScreenRecordingAdapter extends ArrayAdapter<ScreenRecordingAudioSource> { + private LinearLayout mSelectedMic; + private LinearLayout mSelectedInternal; + private LinearLayout mSelectedMicAndInternal; + private LinearLayout mMicOption; + private LinearLayout mMicAndInternalOption; + private LinearLayout mInternalOption; + + public ScreenRecordingAdapter(Context context, int resource, + List<ScreenRecordingAudioSource> objects) { + super(context, resource, objects); + initViews(); + } + + private void initViews() { + mSelectedInternal = getSelected(R.string.screenrecord_device_audio_label); + mSelectedMic = getSelected(R.string.screenrecord_mic_label); + mSelectedMicAndInternal = getSelected(R.string.screenrecord_device_audio_and_mic_label); + + mMicOption = getOption(R.string.screenrecord_mic_label, Resources.ID_NULL); + mMicOption.removeViewAt(1); + + mMicAndInternalOption = getOption( + R.string.screenrecord_device_audio_and_mic_label, Resources.ID_NULL); + mMicAndInternalOption.removeViewAt(1); + + mInternalOption = getOption(R.string.screenrecord_device_audio_label, + R.string.screenrecord_device_audio_description); + } + + private LinearLayout getOption(int label, int description) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + LinearLayout layout = (LinearLayout) inflater + .inflate(R.layout.screen_record_dialog_audio_source, null, false); + ((TextView) layout.findViewById(R.id.screen_recording_dialog_source_text)) + .setText(label); + if (description != Resources.ID_NULL) + ((TextView) layout.findViewById(R.id.screen_recording_dialog_source_description)) + .setText(description); + return layout; + } + + private LinearLayout getSelected(int label) { + LayoutInflater inflater = LayoutInflater.from(getContext()); + LinearLayout layout = (LinearLayout) inflater + .inflate(R.layout.screen_record_dialog_audio_source_selected, null, false); + ((TextView) layout.findViewById(R.id.screen_recording_dialog_source_text)) + .setText(label); + return layout; + } + + private void setDescription(LinearLayout layout, int description) { + if (description != Resources.ID_NULL) { + ((TextView) layout.getChildAt(1)).setText(description); + } + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + switch (getItem(position)) { + case INTERNAL: + return mInternalOption; + case MIC_AND_INTERNAL: + return mMicAndInternalOption; + case MIC: + return mMicOption; + default: + return super.getDropDownView(position, convertView, parent); + } + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + switch (getItem(position)) { + case INTERNAL: + return mSelectedInternal; + case MIC_AND_INTERNAL: + return mSelectedMicAndInternal; + case MIC: + return mSelectedMic; + default: + return super.getView(position, convertView, parent); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAudioSource.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAudioSource.java new file mode 100644 index 000000000000..ee11865a03a1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingAudioSource.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenrecord; + +/** + * Audio sources + */ +public enum ScreenRecordingAudioSource { + NONE, + INTERNAL, + MIC, + MIC_AND_INTERNAL; +} diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingMuxer.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingMuxer.java new file mode 100644 index 000000000000..7ffcfd46a1a4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordingMuxer.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenrecord; + +import android.media.MediaCodec; +import android.media.MediaExtractor; +import android.media.MediaMuxer; +import android.util.ArrayMap; +import android.util.Log; +import android.util.Pair; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; + +/** + * Mixing audio and video tracks + */ +public class ScreenRecordingMuxer { + // size of a memory page for cache coherency + private static final int BUFFER_SIZE = 1024 * 4096; + private String[] mFiles; + private String mOutFile; + private int mFormat; + private ArrayMap<Pair<MediaExtractor, Integer>, Integer> mExtractorIndexToMuxerIndex + = new ArrayMap<>(); + private ArrayList<MediaExtractor> mExtractors = new ArrayList<>(); + + private static String TAG = "ScreenRecordingMuxer"; + public ScreenRecordingMuxer(@MediaMuxer.Format int format, String outfileName, + String... inputFileNames) { + mFiles = inputFileNames; + mOutFile = outfileName; + mFormat = format; + Log.d(TAG, "out: " + mOutFile + " , in: " + mFiles[0]); + } + + /** + * RUN IN THE BACKGROUND THREAD! + */ + public void mux() throws IOException { + MediaMuxer muxer = null; + muxer = new MediaMuxer(mOutFile, mFormat); + // Add extractors + for (String file: mFiles) { + MediaExtractor extractor = new MediaExtractor(); + try { + extractor.setDataSource(file); + } catch (IOException e) { + Log.e(TAG, "error creating extractor: " + file); + e.printStackTrace(); + continue; + } + Log.d(TAG, file + " track count: " + extractor.getTrackCount()); + mExtractors.add(extractor); + for (int i = 0; i < extractor.getTrackCount(); i++) { + int muxId = muxer.addTrack(extractor.getTrackFormat(i)); + Log.d(TAG, "created extractor format" + extractor.getTrackFormat(i).toString()); + mExtractorIndexToMuxerIndex.put(Pair.create(extractor, i), muxId); + } + } + + muxer.start(); + for (Pair<MediaExtractor, Integer> pair: mExtractorIndexToMuxerIndex.keySet()) { + MediaExtractor extractor = pair.first; + extractor.selectTrack(pair.second); + int muxId = mExtractorIndexToMuxerIndex.get(pair); + Log.d(TAG, "track format: " + extractor.getTrackFormat(pair.second)); + extractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC); + ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); + MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); + int offset; + while (true) { + offset = buffer.arrayOffset(); + info.size = extractor.readSampleData(buffer, offset); + if (info.size < 0) break; + info.presentationTimeUs = extractor.getSampleTime(); + info.flags = extractor.getSampleFlags(); + muxer.writeSampleData(muxId, buffer, info); + extractor.advance(); + } + } + + for (MediaExtractor extractor: mExtractors) { + extractor.release(); + } + muxer.stop(); + muxer.release(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java index e67b3d715c84..02a7aca38abe 100644 --- a/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java +++ b/packages/SystemUI/src/com/android/systemui/stackdivider/Divider.java @@ -813,4 +813,12 @@ public class Divider extends SystemUI implements DividerView.DividerCallbacks, updateVisibility(true /* visible */); } } + + /** @return the container token for the secondary split root task. */ + public WindowContainerToken getSecondaryRoot() { + if (mSplits == null || mSplits.mSecondary == null) { + return null; + } + return mSplits.mSecondary.token; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 24195156d8cf..96d6ecbcc07f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -263,7 +263,7 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< default void showAuthenticationDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, int biometricModality, boolean requireConfirmation, int userId, String opPackageName, - long operationId) { } + long operationId, int sysUiSessionId) { } default void onBiometricAuthenticated() { } default void onBiometricHelp(String message) { } default void onBiometricError(int modality, int error, int vendorCode) { } @@ -782,7 +782,7 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< @Override public void showAuthenticationDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, int biometricModality, boolean requireConfirmation, int userId, String opPackageName, - long operationId) { + long operationId, int sysUiSessionId) { synchronized (mLock) { SomeArgs args = SomeArgs.obtain(); args.arg1 = bundle; @@ -792,6 +792,7 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< args.argi2 = userId; args.arg4 = opPackageName; args.arg5 = operationId; + args.argi3 = sysUiSessionId; mHandler.obtainMessage(MSG_BIOMETRIC_SHOW, args) .sendToTarget(); } @@ -1169,7 +1170,8 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< (boolean) someArgs.arg3 /* requireConfirmation */, someArgs.argi2 /* userId */, (String) someArgs.arg4 /* opPackageName */, - (long) someArgs.arg5 /* operationId */); + (long) someArgs.arg5 /* operationId */, + someArgs.argi3 /* sysUiSessionId */); } someArgs.recycle(); break; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index 8fcc67a0708e..e7f26183fa8a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -184,8 +184,11 @@ public class NotificationViewHierarchyManager implements DynamicPrivacyControlle mLowPriorityInflationHelper.recheckLowPriorityViewAndInflate(ent, ent.getRow()); boolean isChildInGroup = mGroupManager.isChildInGroupWithSummary(ent.getSbn()); - boolean groupChangesAllowed = mVisualStabilityManager.areGroupChangesAllowed() - || !ent.hasFinishedInitialization(); + boolean groupChangesAllowed = + mVisualStabilityManager.areGroupChangesAllowed() // user isn't looking at notifs + || !ent.hasFinishedInitialization() // notif recently added + || !mListContainer.containsView(ent.getRow()); // notif recently unfiltered + NotificationEntry parent = mGroupManager.getGroupSummary(ent.getSbn()); if (!groupChangesAllowed) { // We don't to change groups while the user is looking at them diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java index 55a20fae4ffd..040dbe320711 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/InstantAppNotifier.java @@ -288,7 +288,7 @@ public class InstantAppNotifier extends SystemUI mContext, 0, new Intent(Intent.ACTION_VIEW).setData(Uri.parse(helpUrl)), - 0, + PendingIntent.FLAG_IMMUTABLE, null, user) : null; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowController.java index fc6a02840891..567ddb680848 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowController.java @@ -321,7 +321,8 @@ public class NotificationShadeWindowController implements Callback, Dumpable, || state.mPanelVisible || state.mKeyguardFadingAway || state.mBouncerShowing || state.mHeadsUpShowing || state.mScrimsVisibility != ScrimController.TRANSPARENT) - || state.mBackgroundBlurRadius > 0; + || state.mBackgroundBlurRadius > 0 + || state.mLaunchingActivity; } private void applyFitsSystemWindows(State state) { @@ -485,6 +486,11 @@ public class NotificationShadeWindowController implements Callback, Dumpable, apply(mCurrentState); } + void setLaunchingActivity(boolean launching) { + mCurrentState.mLaunchingActivity = launching; + apply(mCurrentState); + } + public void setScrimsVisibility(int scrimsVisibility) { mCurrentState.mScrimsVisibility = scrimsVisibility; apply(mCurrentState); @@ -645,6 +651,7 @@ public class NotificationShadeWindowController implements Callback, Dumpable, boolean mForceCollapsed; boolean mForceDozeBrightness; boolean mForceUserActivity; + boolean mLaunchingActivity; boolean mBackdropShowing; boolean mWallpaperSupportsAmbientMode; boolean mNotTouchable; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java index 596a607bb8ad..0d2589847bcb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java @@ -93,6 +93,7 @@ public class NotificationShadeWindowViewController { private PhoneStatusBarView mStatusBarView; private PhoneStatusBarTransitions mBarTransitions; private StatusBar mService; + private NotificationShadeWindowController mNotificationShadeWindowController; private DragDownHelper mDragDownHelper; private boolean mDoubleTapEnabled; private boolean mSingleTapEnabled; @@ -430,10 +431,14 @@ public class NotificationShadeWindowViewController { public void setExpandAnimationPending(boolean pending) { mExpandAnimationPending = pending; + mNotificationShadeWindowController + .setLaunchingActivity(mExpandAnimationPending | mExpandAnimationRunning); } public void setExpandAnimationRunning(boolean running) { mExpandAnimationRunning = running; + mNotificationShadeWindowController + .setLaunchingActivity(mExpandAnimationPending | mExpandAnimationRunning); } public void cancelExpandHelper() { @@ -456,8 +461,9 @@ public class NotificationShadeWindowViewController { } } - public void setService(StatusBar statusBar) { + public void setService(StatusBar statusBar, NotificationShadeWindowController controller) { mService = statusBar; + mNotificationShadeWindowController = controller; } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index c5901398a54b..33997b9a5735 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -440,24 +440,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo if (!(relevantState && mExpansionAffectsAlpha)) { return; } - applyExpansionToAlpha(); - if (mUpdatePending) { - return; - } - setOrAdaptCurrentAnimation(mScrimBehind); - setOrAdaptCurrentAnimation(mScrimInFront); - setOrAdaptCurrentAnimation(mScrimForBubble); - dispatchScrimState(mScrimBehind.getViewAlpha()); - - // Reset wallpaper timeout if it's already timeout like expanding panel while PULSING - // and docking. - if (mWallpaperVisibilityTimedOut) { - mWallpaperVisibilityTimedOut = false; - DejankUtils.postAfterTraversal(() -> { - mTimeTicker.schedule(mDozeParameters.getWallpaperAodDuration(), - AlarmTimeout.MODE_IGNORE_IF_SCHEDULED); - }); - } + applyAndDispatchExpansion(); } } @@ -513,6 +496,27 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo } } + private void applyAndDispatchExpansion() { + applyExpansionToAlpha(); + if (mUpdatePending) { + return; + } + setOrAdaptCurrentAnimation(mScrimBehind); + setOrAdaptCurrentAnimation(mScrimInFront); + setOrAdaptCurrentAnimation(mScrimForBubble); + dispatchScrimState(mScrimBehind.getViewAlpha()); + + // Reset wallpaper timeout if it's already timeout like expanding panel while PULSING + // and docking. + if (mWallpaperVisibilityTimedOut) { + mWallpaperVisibilityTimedOut = false; + DejankUtils.postAfterTraversal(() -> { + mTimeTicker.schedule(mDozeParameters.getWallpaperAodDuration(), + AlarmTimeout.MODE_IGNORE_IF_SCHEDULED); + }); + } + } + /** * Sets the given drawable as the background of the scrim that shows up behind the * notifications. @@ -1006,6 +1010,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, OnCo public void setExpansionAffectsAlpha(boolean expansionAffectsAlpha) { mExpansionAffectsAlpha = expansionAffectsAlpha; + if (expansionAffectsAlpha) { + applyAndDispatchExpansion(); + } } public void setKeyguardOccluded(boolean keyguardOccluded) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index bbf83bc2057a..dd54a3d800fb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -1001,7 +1001,7 @@ public class StatusBar extends SystemUI implements DemoMode, updateTheme(); inflateStatusBarWindow(); - mNotificationShadeWindowViewController.setService(this); + mNotificationShadeWindowViewController.setService(this, mNotificationShadeWindowController); mNotificationShadeWindowView.setOnTouchListener(getStatusBarWindowTouchListener()); // TODO: Deal with the ugliness that comes from having some of the statusbar broken out diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java index 8acfbf2b6996..7729965b56c4 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/ConcurrencyModule.java @@ -23,6 +23,7 @@ import android.os.Looper; import android.os.Process; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.LongRunning; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; @@ -50,6 +51,17 @@ public abstract class ConcurrencyModule { return thread.getLooper(); } + /** Long running tasks Looper */ + @Provides + @Singleton + @LongRunning + public static Looper provideLongRunningLooper() { + HandlerThread thread = new HandlerThread("SysUiLng", + Process.THREAD_PRIORITY_BACKGROUND); + thread.start(); + return thread.getLooper(); + } + /** Main Looper */ @Provides @Main @@ -89,6 +101,16 @@ public abstract class ConcurrencyModule { } /** + * Provide a Long running Executor by default. + */ + @Provides + @Singleton + @LongRunning + public static Executor provideLongRunningExecutor(@LongRunning Looper looper) { + return new ExecutorImpl(looper); + } + + /** * Provide a Background-Thread Executor. */ @Provides diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index fc1ddf74a448..821b8504e5f7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -470,7 +470,8 @@ public class AuthControllerTest extends SysuiTestCase { true /* requireConfirmation */, 0 /* userId */, "testPackage", - 0 /* operationId */); + 0 /* operationId */, + 0 /* sysUiSessionId */); } private Bundle createTestDialogBundle(int authenticators) { @@ -508,7 +509,7 @@ public class AuthControllerTest extends SysuiTestCase { @Override protected AuthDialog buildDialog(Bundle biometricPromptBundle, boolean requireConfirmation, int userId, int type, String opPackageName, - boolean skipIntro, long operationId) { + boolean skipIntro, long operationId, int sysUiSessionId) { mLastBiometricPromptBundle = biometricPromptBundle; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java index cffcabb55e14..63e6e8cba628 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -410,11 +410,12 @@ public class CommandQueueTest extends SysuiTestCase { Bundle bundle = new Bundle(); String packageName = "test"; final long operationId = 1; + final int sysUiSessionId = 2; mCommandQueue.showAuthenticationDialog(bundle, null /* receiver */, 1, true, 3, - packageName, operationId); + packageName, operationId, sysUiSessionId); waitForIdleSync(); verify(mCallbacks).showAuthenticationDialog(eq(bundle), eq(null), eq(1), eq(true), eq(3), - eq(packageName), eq(operationId)); + eq(packageName), eq(operationId), eq(sysUiSessionId)); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java index cc2d1c25de38..e04d25b17c71 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java @@ -83,6 +83,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { @Mock private NotificationStackScrollLayout mNotificationStackScrollLayout; @Mock private NotificationShadeDepthController mNotificationShadeDepthController; @Mock private SuperStatusBarViewFactory mStatusBarViewFactory; + @Mock private NotificationShadeWindowController mNotificationShadeWindowController; @Before public void setUp() { @@ -121,7 +122,7 @@ public class NotificationShadeWindowViewTest extends SysuiTestCase { mNotificationPanelViewController, mStatusBarViewFactory); mController.setupExpandedStatusBar(); - mController.setService(mStatusBar); + mController.setService(mStatusBar, mNotificationShadeWindowController); mController.setDragDownHelper(mDragDownHelper); } diff --git a/packages/Tethering/Android.bp b/packages/Tethering/Android.bp index bfb65241ec6d..cbc5e14139ac 100644 --- a/packages/Tethering/Android.bp +++ b/packages/Tethering/Android.bp @@ -27,7 +27,7 @@ java_defaults { "androidx.annotation_annotation", "netd_aidl_interface-V3-java", "netlink-client", - "networkstack-aidl-interfaces-unstable-java", + "networkstack-aidl-interfaces-java", "android.hardware.tetheroffload.config-V1.0-java", "android.hardware.tetheroffload.control-V1.0-java", "net-utils-framework-common", @@ -109,6 +109,7 @@ android_app { manifest: "AndroidManifest_InProcess.xml", // InProcessTethering is a replacement for Tethering overrides: ["Tethering"], + apex_available: ["com.android.tethering"], } // Updatable tethering packaged as an application diff --git a/packages/Tethering/apex/Android.bp b/packages/Tethering/apex/Android.bp index 24df5f696077..20ccd2ad6453 100644 --- a/packages/Tethering/apex/Android.bp +++ b/packages/Tethering/apex/Android.bp @@ -36,3 +36,12 @@ android_app_certificate { name: "com.android.tethering.certificate", certificate: "com.android.tethering", } + +override_apex { + name: "com.android.tethering.inprocess", + base: "com.android.tethering", + package_name: "com.android.tethering.inprocess", + apps: [ + "InProcessTethering", + ], +} diff --git a/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java b/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java index 952325cc4380..b2a43c47d1c6 100644 --- a/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java +++ b/packages/Tethering/src/com/android/networkstack/tethering/Tethering.java @@ -62,7 +62,6 @@ import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE; -import android.app.usage.NetworkStatsManager; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothPan; import android.bluetooth.BluetoothProfile; @@ -268,12 +267,9 @@ public class Tethering { mTetherMasterSM = new TetherMasterSM("TetherMaster", mLooper, deps); mTetherMasterSM.start(); - final NetworkStatsManager statsManager = - (NetworkStatsManager) mContext.getSystemService(Context.NETWORK_STATS_SERVICE); mHandler = mTetherMasterSM.getHandler(); - mOffloadController = new OffloadController(mHandler, - mDeps.getOffloadHardwareInterface(mHandler, mLog), mContext.getContentResolver(), - statsManager, mLog, new OffloadController.Dependencies() { + mOffloadController = mDeps.getOffloadController(mHandler, mLog, + new OffloadController.Dependencies() { @Override public TetheringConfiguration getTetherConfig() { diff --git a/packages/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/packages/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java index 9b54b5ff2403..802f2acb3310 100644 --- a/packages/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java +++ b/packages/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java @@ -16,6 +16,7 @@ package com.android.networkstack.tethering; +import android.app.usage.NetworkStatsManager; import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.net.INetd; @@ -47,6 +48,19 @@ public abstract class TetheringDependencies { } /** + * Get a reference to the offload controller to be used by tethering. + */ + @NonNull + public OffloadController getOffloadController(@NonNull Handler h, + @NonNull SharedLog log, @NonNull OffloadController.Dependencies deps) { + final NetworkStatsManager statsManager = + (NetworkStatsManager) getContext().getSystemService(Context.NETWORK_STATS_SERVICE); + return new OffloadController(h, getOffloadHardwareInterface(h, log), + getContext().getContentResolver(), statsManager, log, deps); + } + + + /** * Get a reference to the UpstreamNetworkMonitor to be used by tethering. */ public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx, StateMachine target, diff --git a/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java index 0363f5f9989f..fff7a70f54d0 100644 --- a/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java +++ b/packages/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java @@ -150,6 +150,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.net.Inet4Address; import java.net.Inet6Address; import java.util.ArrayList; @@ -212,6 +214,9 @@ public class TetheringTest { private Tethering mTethering; private PhoneStateListener mPhoneStateListener; private InterfaceConfigurationParcel mInterfaceConfiguration; + private TetheringConfiguration mConfig; + private EntitlementManager mEntitleMgr; + private OffloadController mOffloadCtrl; private class TestContext extends BroadcastInterceptingContext { TestContext(Context base) { @@ -297,8 +302,9 @@ public class TetheringTest { } } - private class MockTetheringConfiguration extends TetheringConfiguration { - MockTetheringConfiguration(Context ctx, SharedLog log, int id) { + // MyTetheringConfiguration is used to override static method for testing. + private class MyTetheringConfiguration extends TetheringConfiguration { + MyTetheringConfiguration(Context ctx, SharedLog log, int id) { super(ctx, log, id); } @@ -328,6 +334,15 @@ public class TetheringTest { } @Override + public OffloadController getOffloadController(Handler h, SharedLog log, + OffloadController.Dependencies deps) { + mOffloadCtrl = spy(super.getOffloadController(h, log, deps)); + // Return real object here instead of mock because + // testReportFailCallbackIfOffloadNotSupported depend on real OffloadController object. + return mOffloadCtrl; + } + + @Override public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx, StateMachine target, SharedLog log, int what) { mUpstreamNetworkMonitorMasterSM = target; @@ -352,6 +367,13 @@ public class TetheringTest { } @Override + public EntitlementManager getEntitlementManager(Context ctx, StateMachine target, + SharedLog log, int what) { + mEntitleMgr = spy(super.getEntitlementManager(ctx, target, log, what)); + return mEntitleMgr; + } + + @Override public boolean isTetheringSupported() { return true; } @@ -359,7 +381,8 @@ public class TetheringTest { @Override public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log, int subId) { - return new MockTetheringConfiguration(ctx, log, subId); + mConfig = spy(new MyTetheringConfiguration(ctx, log, subId)); + return mConfig; } @Override @@ -1726,6 +1749,17 @@ public class TetheringTest { verify(mNotificationUpdater, never()).onUpstreamCapabilitiesChanged(any()); } + @Test + public void testDumpTetheringLog() throws Exception { + final FileDescriptor mockFd = mock(FileDescriptor.class); + final PrintWriter mockPw = mock(PrintWriter.class); + runUsbTethering(null); + mTethering.dump(mockFd, mockPw, new String[0]); + verify(mConfig).dump(any()); + verify(mEntitleMgr).dump(any()); + verify(mOffloadCtrl).dump(any()); + } + // TODO: Test that a request for hotspot mode doesn't interfere with an // already operating tethering mode interface. } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java index edb4445151d5..0f98992d1ea0 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java @@ -94,8 +94,10 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect if (userState == null) return; final long identity = Binder.clearCallingIdentity(); try { - int flags = Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE - | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS; + int flags = Context.BIND_AUTO_CREATE + | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE + | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS + | Context.BIND_INCLUDE_CAPABILITIES; if (userState.getBindInstantServiceAllowedLocked()) { flags |= Context.BIND_ALLOW_INSTANT; } diff --git a/services/autofill/java/com/android/server/autofill/AutofillInlineSuggestionsRequestSession.java b/services/autofill/java/com/android/server/autofill/AutofillInlineSuggestionsRequestSession.java index ce11c76e5c6a..22451e1d992e 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillInlineSuggestionsRequestSession.java +++ b/services/autofill/java/com/android/server/autofill/AutofillInlineSuggestionsRequestSession.java @@ -37,6 +37,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.view.IInlineSuggestionsRequestCallback; import com.android.internal.view.IInlineSuggestionsResponseCallback; import com.android.internal.view.InlineSuggestionsRequestInfo; +import com.android.server.autofill.ui.InlineSuggestionFactory; import com.android.server.inputmethod.InputMethodManagerInternal; import java.lang.ref.WeakReference; @@ -242,7 +243,8 @@ final class AutofillInlineSuggestionsRequestSession { } if (sDebug) Log.d(TAG, "Send inline response: " + response.getInlineSuggestions().size()); try { - mResponseCallback.onInlineSuggestionsResponse(mAutofillId, response); + mResponseCallback.onInlineSuggestionsResponse(mAutofillId, + InlineSuggestionFactory.copy(response)); } catch (RemoteException e) { Slog.e(TAG, "RemoteException sending InlineSuggestionsResponse to IME"); } diff --git a/services/autofill/java/com/android/server/autofill/RemoteInlineSuggestionRenderService.java b/services/autofill/java/com/android/server/autofill/RemoteInlineSuggestionRenderService.java index 255adcd92da3..617c111c6c38 100644 --- a/services/autofill/java/com/android/server/autofill/RemoteInlineSuggestionRenderService.java +++ b/services/autofill/java/com/android/server/autofill/RemoteInlineSuggestionRenderService.java @@ -47,7 +47,7 @@ public final class RemoteInlineSuggestionRenderService extends private static final String TAG = "RemoteInlineSuggestionRenderService"; - private final int mIdleUnbindTimeoutMs = 5000; + private final long mIdleUnbindTimeoutMs = PERMANENT_BOUND_TIMEOUT_MS; RemoteInlineSuggestionRenderService(Context context, ComponentName componentName, String serviceInterface, int userId, InlineSuggestionRenderCallbacks callback, diff --git a/services/autofill/java/com/android/server/autofill/ui/InlineContentProviderImpl.java b/services/autofill/java/com/android/server/autofill/ui/InlineContentProviderImpl.java new file mode 100644 index 000000000000..819f2b813a5e --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/ui/InlineContentProviderImpl.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.autofill.ui; + +import static com.android.server.autofill.Helper.sVerbose; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Handler; +import android.util.Slog; + +import com.android.internal.view.inline.IInlineContentCallback; +import com.android.internal.view.inline.IInlineContentProvider; +import com.android.server.FgThread; + +/** + * We create one instance of this class for each {@link android.view.inputmethod.InlineSuggestion} + * instance. Each inline suggestion instance will only be sent to the remote IME process once. In + * case of filtering and resending the suggestion when keyboard state changes between hide and + * show, a new instance of this class will be created using {@link #copy()}, with the same backing + * {@link RemoteInlineSuggestionUi}. When the + * {@link #provideContent(int, int, IInlineContentCallback)} is called the first time (it's only + * allowed to be called at most once), the passed in width/height is used to determine whether + * the existing {@link RemoteInlineSuggestionUi} provided in the constructor can be reused, or a + * new one should be created to suit the new size requirement for the view. In normal cases, + * we should not expect the size requirement to change, although in theory the public API allows + * the IME to do that. + * + * <p>This design is to enable us to be able to reuse the backing remote view while still keeping + * the callbacks relatively well aligned. For example, if we allow multiple remote IME binder + * callbacks to call into one instance of this class, then binder A may call in with width/height + * X for which we create a view (i.e. {@link RemoteInlineSuggestionUi}) for it, + * + * See also {@link RemoteInlineSuggestionUi} for relevant information. + */ +public final class InlineContentProviderImpl extends IInlineContentProvider.Stub { + + // TODO(b/153615023): consider not holding strong reference to heavy objects in this stub, to + // avoid memory leak in case the client app is holding the remote reference for a longer + // time than expected. Essentially we need strong reference in the system process to + // the member variables, but weak reference to them in the IInlineContentProvider.Stub. + + private static final String TAG = InlineContentProviderImpl.class.getSimpleName(); + + private final Handler mHandler = FgThread.getHandler();; + + @NonNull + private final RemoteInlineSuggestionViewConnector mRemoteInlineSuggestionViewConnector; + @Nullable + private RemoteInlineSuggestionUi mRemoteInlineSuggestionUi; + + private boolean mProvideContentCalled = false; + + InlineContentProviderImpl( + @NonNull RemoteInlineSuggestionViewConnector remoteInlineSuggestionViewConnector, + @Nullable RemoteInlineSuggestionUi remoteInlineSuggestionUi) { + mRemoteInlineSuggestionViewConnector = remoteInlineSuggestionViewConnector; + mRemoteInlineSuggestionUi = remoteInlineSuggestionUi; + } + + /** + * Returns a new instance of this class, with the same {@code mInlineSuggestionRenderer} and + * {@code mRemoteInlineSuggestionUi}. The latter may or may not be reusable depending on the + * size information provided when the client calls {@link #provideContent(int, int, + * IInlineContentCallback)}. + */ + @NonNull + public InlineContentProviderImpl copy() { + return new InlineContentProviderImpl(mRemoteInlineSuggestionViewConnector, + mRemoteInlineSuggestionUi); + } + + /** + * Provides a SurfacePackage associated with the inline suggestion view to the IME. If such + * view doesn't exit, then create a new one. This method should be called once per lifecycle + * of this object. Any further calls to the method will be ignored. + */ + @Override + public void provideContent(int width, int height, IInlineContentCallback callback) { + mHandler.post(() -> handleProvideContent(width, height, callback)); + } + + @Override + public void requestSurfacePackage() { + mHandler.post(this::handleGetSurfacePackage); + } + + @Override + public void onSurfacePackageReleased() { + mHandler.post(this::handleOnSurfacePackageReleased); + } + + private void handleProvideContent(int width, int height, IInlineContentCallback callback) { + if (sVerbose) Slog.v(TAG, "handleProvideContent"); + if (mProvideContentCalled) { + // This method should only be called once. + return; + } + mProvideContentCalled = true; + if (mRemoteInlineSuggestionUi == null || !mRemoteInlineSuggestionUi.match(width, height)) { + mRemoteInlineSuggestionUi = new RemoteInlineSuggestionUi( + mRemoteInlineSuggestionViewConnector, + width, height, mHandler); + } + mRemoteInlineSuggestionUi.setInlineContentCallback(callback); + mRemoteInlineSuggestionUi.requestSurfacePackage(); + } + + private void handleGetSurfacePackage() { + if (sVerbose) Slog.v(TAG, "handleGetSurfacePackage"); + if (!mProvideContentCalled || mRemoteInlineSuggestionUi == null) { + // provideContent should be called first, and remote UI should not be null. + return; + } + mRemoteInlineSuggestionUi.requestSurfacePackage(); + } + + private void handleOnSurfacePackageReleased() { + if (sVerbose) Slog.v(TAG, "handleOnSurfacePackageReleased"); + if (!mProvideContentCalled || mRemoteInlineSuggestionUi == null) { + // provideContent should be called first, and remote UI should not be null. + return; + } + mRemoteInlineSuggestionUi.surfacePackageReleased(); + } +} diff --git a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java index 79c9efa48d73..e74463a8584b 100644 --- a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java +++ b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java @@ -24,14 +24,11 @@ import android.annotation.Nullable; import android.content.Intent; import android.content.IntentSender; import android.os.IBinder; -import android.os.RemoteException; import android.service.autofill.Dataset; import android.service.autofill.FillResponse; -import android.service.autofill.IInlineSuggestionUiCallback; import android.service.autofill.InlinePresentation; import android.text.TextUtils; import android.util.Slog; -import android.view.SurfaceControlViewHost; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; import android.view.autofill.AutofillValue; @@ -41,12 +38,8 @@ import android.view.inputmethod.InlineSuggestionsRequest; import android.view.inputmethod.InlineSuggestionsResponse; import android.widget.inline.InlinePresentationSpec; -import com.android.internal.view.inline.IInlineContentCallback; import com.android.internal.view.inline.IInlineContentProvider; -import com.android.server.LocalServices; -import com.android.server.UiThread; import com.android.server.autofill.RemoteInlineSuggestionRenderService; -import com.android.server.inputmethod.InputMethodManagerInternal; import java.util.ArrayList; import java.util.List; @@ -73,6 +66,27 @@ public final class InlineSuggestionFactory { } /** + * Returns a copy of the response, that internally copies the {@link IInlineContentProvider} + * so that it's not reused by the remote IME process across different inline suggestions. + * See {@link InlineContentProviderImpl} for why this is needed. + */ + @NonNull + public static InlineSuggestionsResponse copy(@NonNull InlineSuggestionsResponse response) { + final ArrayList<InlineSuggestion> copiedInlineSuggestions = new ArrayList<>(); + for (InlineSuggestion inlineSuggestion : response.getInlineSuggestions()) { + final IInlineContentProvider contentProvider = inlineSuggestion.getContentProvider(); + if (contentProvider instanceof InlineContentProviderImpl) { + copiedInlineSuggestions.add(new + InlineSuggestion(inlineSuggestion.getInfo(), + ((InlineContentProviderImpl) contentProvider).copy())); + } else { + copiedInlineSuggestions.add(inlineSuggestion); + } + } + return new InlineSuggestionsResponse(copiedInlineSuggestions); + } + + /** * Creates an {@link InlineSuggestionsResponse} with the {@code datasets} provided by the * autofill service, potentially filtering the datasets. */ @@ -276,78 +290,20 @@ public final class InlineSuggestionFactory { inlinePresentation.isPinned()); } - private static IInlineContentProvider.Stub createInlineContentProvider( + private static IInlineContentProvider createInlineContentProvider( @NonNull InlinePresentation inlinePresentation, @Nullable Runnable onClickAction, @NonNull Runnable onErrorCallback, @NonNull Consumer<IntentSender> intentSenderConsumer, @Nullable RemoteInlineSuggestionRenderService remoteRenderService, @Nullable IBinder hostInputToken, int displayId) { - return new IInlineContentProvider.Stub() { - @Override - public void provideContent(int width, int height, IInlineContentCallback callback) { - UiThread.getHandler().post(() -> { - final IInlineSuggestionUiCallback uiCallback = createInlineSuggestionUiCallback( - callback, onClickAction, onErrorCallback, intentSenderConsumer); - - if (remoteRenderService == null) { - Slog.e(TAG, "RemoteInlineSuggestionRenderService is null"); - return; - } - - remoteRenderService.renderSuggestion(uiCallback, inlinePresentation, - width, height, hostInputToken, displayId); - }); - } - }; - } - - private static IInlineSuggestionUiCallback.Stub createInlineSuggestionUiCallback( - @NonNull IInlineContentCallback callback, @NonNull Runnable onAutofillCallback, - @NonNull Runnable onErrorCallback, - @NonNull Consumer<IntentSender> intentSenderConsumer) { - return new IInlineSuggestionUiCallback.Stub() { - @Override - public void onClick() throws RemoteException { - onAutofillCallback.run(); - callback.onClick(); - } - - @Override - public void onLongClick() throws RemoteException { - callback.onLongClick(); - } - - @Override - public void onContent(SurfaceControlViewHost.SurfacePackage surface, int width, - int height) - throws RemoteException { - callback.onContent(surface, width, height); - surface.release(); - } - - @Override - public void onError() throws RemoteException { - onErrorCallback.run(); - } - - @Override - public void onTransferTouchFocusToImeWindow(IBinder sourceInputToken, int displayId) - throws RemoteException { - final InputMethodManagerInternal inputMethodManagerInternal = - LocalServices.getService(InputMethodManagerInternal.class); - if (!inputMethodManagerInternal.transferTouchFocusToImeWindow(sourceInputToken, - displayId)) { - Slog.e(TAG, "Cannot transfer touch focus from suggestion to IME"); - onErrorCallback.run(); - } - } - - @Override - public void onStartIntentSender(IntentSender intentSender) { - intentSenderConsumer.accept(intentSender); - } - }; + RemoteInlineSuggestionViewConnector + remoteInlineSuggestionViewConnector = new RemoteInlineSuggestionViewConnector( + remoteRenderService, inlinePresentation, hostInputToken, displayId, onClickAction, + onErrorCallback, intentSenderConsumer); + InlineContentProviderImpl inlineContentProvider = new InlineContentProviderImpl( + remoteInlineSuggestionViewConnector, null); + return inlineContentProvider; } private InlineSuggestionFactory() { diff --git a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionUi.java b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionUi.java new file mode 100644 index 000000000000..00a5283c9b1f --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionUi.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.autofill.ui; + +import static com.android.server.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.IntentSender; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.service.autofill.IInlineSuggestionUi; +import android.service.autofill.IInlineSuggestionUiCallback; +import android.service.autofill.ISurfacePackageResultCallback; +import android.util.Slog; +import android.view.SurfaceControlViewHost; + +import com.android.internal.view.inline.IInlineContentCallback; + +/** + * The instance of this class lives in the system server, orchestrating the communication between + * the remote process owning embedded view (i.e. ExtServices) and the remote process hosting the + * embedded view (i.e. IME). It's also responsible for releasing the embedded view from the owning + * process when it's not longer needed in the hosting process. + * + * <p>An instance of this class may be reused to associate with multiple instances of + * {@link InlineContentProviderImpl}s, each of which wraps a callback from the IME. But at any + * given time, there is only one active IME callback which this class will callback into. + * + * <p>This class is thread safe, because all the outside calls are piped into the same single + * thread handler to be processed. + * + * TODO(b/154683107): implement the reference counting in case there are multiple active + * SurfacePackages at the same time. This will not happen for now since all the InlineSuggestions + * sharing the same UI will be sent to the same IME window, so the previous view will be detached + * before the new view are attached to the window. + */ +final class RemoteInlineSuggestionUi { + + private static final String TAG = RemoteInlineSuggestionUi.class.getSimpleName(); + + // The delay time to release the remote inline suggestion view (in the renderer + // process) after receiving a signal about the surface package being released due to being + // detached from the window in the host app (in the IME process). The release will be + // canceled if the host app reattaches the view to a window within this delay time. + // TODO(b/154683107): try out using the Chroreographer to schedule the release right at the + // next frame. Basically if the view is not re-attached to the window immediately in the next + // frame after it was detached, then it will be released. + private static final long RELEASE_REMOTE_VIEW_HOST_DELAY_MS = 200; + + @NonNull + private final Handler mHandler; + @NonNull + private final RemoteInlineSuggestionViewConnector mRemoteInlineSuggestionViewConnector; + private final int mWidth; + private final int mHeight; + @NonNull + private final InlineSuggestionUiCallbackImpl mInlineSuggestionUiCallback; + + @Nullable + private IInlineContentCallback mInlineContentCallback; // from IME + + /** + * Remote inline suggestion view, backed by an instance of {@link SurfaceControlViewHost} in + * the render service process. We takes care of releasing it when there is no remote + * reference to it (from IME), and we will create a new instance of the view when it's needed + * by IME again. + */ + @Nullable + private IInlineSuggestionUi mInlineSuggestionUi; + private boolean mWaitingForUiCreation = false; + private int mActualWidth; + private int mActualHeight; + + @Nullable + private Runnable mDelayedReleaseViewRunnable; + + RemoteInlineSuggestionUi( + @NonNull RemoteInlineSuggestionViewConnector remoteInlineSuggestionViewConnector, + int width, int height, Handler handler) { + mHandler = handler; + mRemoteInlineSuggestionViewConnector = remoteInlineSuggestionViewConnector; + mWidth = width; + mHeight = height; + mInlineSuggestionUiCallback = new InlineSuggestionUiCallbackImpl(); + } + + /** + * Updates the callback from the IME process. It'll swap out the previous IME callback, and + * all the subsequent callback events (onClick, onLongClick, touch event transfer, etc) will + * be directed to the new callback. + */ + void setInlineContentCallback(@NonNull IInlineContentCallback inlineContentCallback) { + mHandler.post(() -> { + mInlineContentCallback = inlineContentCallback; + }); + } + + /** + * Handles the request from the IME process to get a new surface package. May create a new + * view in the renderer process if the existing view is already released. + */ + void requestSurfacePackage() { + mHandler.post(this::handleRequestSurfacePackage); + } + + /** + * Handles the signal from the IME process that the previously sent surface package has been + * released. + */ + void surfacePackageReleased() { + mHandler.post(this::handleSurfacePackageReleased); + } + + /** + * Returns true if the provided size matches the remote view's size. + */ + boolean match(int width, int height) { + return mWidth == width && mHeight == height; + } + + private void handleSurfacePackageReleased() { + cancelPendingReleaseViewRequest(); + + // Schedule a delayed release view request + mDelayedReleaseViewRunnable = () -> { + if (mInlineSuggestionUi != null) { + try { + mInlineSuggestionUi.releaseSurfaceControlViewHost(); + mInlineSuggestionUi = null; + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException calling releaseSurfaceControlViewHost"); + } + } + mDelayedReleaseViewRunnable = null; + }; + mHandler.postDelayed(mDelayedReleaseViewRunnable, RELEASE_REMOTE_VIEW_HOST_DELAY_MS); + } + + private void handleRequestSurfacePackage() { + cancelPendingReleaseViewRequest(); + + if (mInlineSuggestionUi == null) { + if (mWaitingForUiCreation) { + // This could happen in the following case: the remote embedded view was released + // when previously detached from window. An event after that to re-attached to + // the window will cause us calling the renderSuggestion again. Now, before the + // render call returns a new surface package, if the view is detached and + // re-attached to the window, causing this method to be called again, we will get + // to this state. This request will be ignored and the surface package will still + // be sent back once the view is rendered. + if (sDebug) Slog.d(TAG, "Inline suggestion ui is not ready"); + } else { + mRemoteInlineSuggestionViewConnector.renderSuggestion(mWidth, mHeight, + mInlineSuggestionUiCallback); + mWaitingForUiCreation = true; + } + } else { + try { + mInlineSuggestionUi.getSurfacePackage(new ISurfacePackageResultCallback.Stub() { + @Override + public void onResult(SurfaceControlViewHost.SurfacePackage result) + throws RemoteException { + if (sDebug) Slog.d(TAG, "Sending new SurfacePackage to IME"); + mInlineContentCallback.onContent(result, mActualWidth, mActualHeight); + } + }); + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException calling getSurfacePackage."); + } + } + } + + private void cancelPendingReleaseViewRequest() { + if (mDelayedReleaseViewRunnable != null) { + mHandler.removeCallbacks(mDelayedReleaseViewRunnable); + mDelayedReleaseViewRunnable = null; + } + } + + /** + * This is called when a new inline suggestion UI is inflated from the ext services. + */ + private void handleInlineSuggestionUiReady(IInlineSuggestionUi content, + SurfaceControlViewHost.SurfacePackage surfacePackage, int width, int height) { + mInlineSuggestionUi = content; + mWaitingForUiCreation = false; + mActualWidth = width; + mActualHeight = height; + if (mInlineContentCallback != null) { + try { + mInlineContentCallback.onContent(surfacePackage, mActualWidth, mActualHeight); + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException calling onContent"); + } + } + if (surfacePackage != null) { + surfacePackage.release(); + } + } + + private void handleOnClick() { + // Autofill the value + mRemoteInlineSuggestionViewConnector.onClick(); + + // Notify the remote process (IME) that hosts the embedded UI that it's clicked + if (mInlineContentCallback != null) { + try { + mInlineContentCallback.onClick(); + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException calling onClick"); + } + } + } + + private void handleOnLongClick() { + // Notify the remote process (IME) that hosts the embedded UI that it's long clicked + if (mInlineContentCallback != null) { + try { + mInlineContentCallback.onLongClick(); + } catch (RemoteException e) { + Slog.w(TAG, "RemoteException calling onLongClick"); + } + } + } + + private void handleOnError() { + mRemoteInlineSuggestionViewConnector.onError(); + } + + private void handleOnTransferTouchFocusToImeWindow(IBinder sourceInputToken, int displayId) { + mRemoteInlineSuggestionViewConnector.onTransferTouchFocusToImeWindow(sourceInputToken, + displayId); + } + + private void handleOnStartIntentSender(IntentSender intentSender) { + mRemoteInlineSuggestionViewConnector.onStartIntentSender(intentSender); + } + + /** + * Responsible for communicating with the inline suggestion view owning process. + */ + private class InlineSuggestionUiCallbackImpl extends IInlineSuggestionUiCallback.Stub { + + @Override + public void onClick() { + mHandler.post(RemoteInlineSuggestionUi.this::handleOnClick); + } + + @Override + public void onLongClick() { + mHandler.post(RemoteInlineSuggestionUi.this::handleOnLongClick); + } + + @Override + public void onContent(IInlineSuggestionUi content, + SurfaceControlViewHost.SurfacePackage surface, int width, int height) { + mHandler.post(() -> handleInlineSuggestionUiReady(content, surface, width, height)); + } + + @Override + public void onError() { + mHandler.post(RemoteInlineSuggestionUi.this::handleOnError); + } + + @Override + public void onTransferTouchFocusToImeWindow(IBinder sourceInputToken, int displayId) { + mHandler.post(() -> handleOnTransferTouchFocusToImeWindow(sourceInputToken, displayId)); + } + + @Override + public void onStartIntentSender(IntentSender intentSender) { + mHandler.post(() -> handleOnStartIntentSender(intentSender)); + } + } +} diff --git a/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java new file mode 100644 index 000000000000..9d23c171800d --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/ui/RemoteInlineSuggestionViewConnector.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.autofill.ui; + +import static com.android.server.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.IntentSender; +import android.os.IBinder; +import android.service.autofill.IInlineSuggestionUiCallback; +import android.service.autofill.InlinePresentation; +import android.util.Slog; + +import com.android.server.LocalServices; +import com.android.server.autofill.RemoteInlineSuggestionRenderService; +import com.android.server.inputmethod.InputMethodManagerInternal; + +import java.util.function.Consumer; + +/** + * Wraps the parameters needed to create a new inline suggestion view in the remote renderer + * service, and handles the callback from the events on the created remote view. + */ +final class RemoteInlineSuggestionViewConnector { + private static final String TAG = RemoteInlineSuggestionViewConnector.class.getSimpleName(); + + @Nullable + private final RemoteInlineSuggestionRenderService mRemoteRenderService; + @NonNull + private final InlinePresentation mInlinePresentation; + @Nullable + private final IBinder mHostInputToken; + private final int mDisplayId; + + @NonNull + private final Runnable mOnAutofillCallback; + @NonNull + private final Runnable mOnErrorCallback; + @NonNull + private final Consumer<IntentSender> mStartIntentSenderFromClientApp; + + RemoteInlineSuggestionViewConnector( + @Nullable RemoteInlineSuggestionRenderService remoteRenderService, + @NonNull InlinePresentation inlinePresentation, + @Nullable IBinder hostInputToken, + int displayId, + @NonNull Runnable onAutofillCallback, + @NonNull Runnable onErrorCallback, + @NonNull Consumer<IntentSender> startIntentSenderFromClientApp) { + mRemoteRenderService = remoteRenderService; + mInlinePresentation = inlinePresentation; + mHostInputToken = hostInputToken; + mDisplayId = displayId; + + mOnAutofillCallback = onAutofillCallback; + mOnErrorCallback = onErrorCallback; + mStartIntentSenderFromClientApp = startIntentSenderFromClientApp; + } + + /** + * Calls the remote renderer service to create a new inline suggestion view. + * + * @return true if the call is made to the remote renderer service, false otherwise. + */ + public boolean renderSuggestion(int width, int height, + @NonNull IInlineSuggestionUiCallback callback) { + if (mRemoteRenderService != null) { + if (sDebug) Slog.d(TAG, "Request to recreate the UI"); + mRemoteRenderService.renderSuggestion(callback, mInlinePresentation, width, height, + mHostInputToken, mDisplayId); + return true; + } + return false; + } + + /** + * Handles the callback for the event of remote view being clicked. + */ + public void onClick() { + mOnAutofillCallback.run(); + } + + /** + * Handles the callback for the remote error when creating or interacting with the view. + */ + public void onError() { + mOnErrorCallback.run(); + } + + /** + * Handles the callback for transferring the touch event on the remote view to the IME + * process. + */ + public void onTransferTouchFocusToImeWindow(IBinder sourceInputToken, int displayId) { + final InputMethodManagerInternal inputMethodManagerInternal = + LocalServices.getService(InputMethodManagerInternal.class); + if (!inputMethodManagerInternal.transferTouchFocusToImeWindow(sourceInputToken, + displayId)) { + Slog.e(TAG, "Cannot transfer touch focus from suggestion to IME"); + mOnErrorCallback.run(); + } + } + + /** + * Handles starting an intent sender from the client app's process. + */ + public void onStartIntentSender(IntentSender intentSender) { + mStartIntentSenderFromClientApp.accept(intentSender); + } +} diff --git a/services/core/java/com/android/server/RescueParty.java b/services/core/java/com/android/server/RescueParty.java index bfcde97d6c91..829fca66ec0d 100644 --- a/services/core/java/com/android/server/RescueParty.java +++ b/services/core/java/com/android/server/RescueParty.java @@ -253,10 +253,7 @@ public class RescueParty { logCriticalInfo(Log.DEBUG, "Finished rescue level " + levelToString(level)); } catch (Throwable t) { - final String msg = ExceptionUtils.getCompleteMessage(t); - EventLogTags.writeRescueFailure(level, msg); - logCriticalInfo(Log.ERROR, - "Failed rescue level " + levelToString(level) + ": " + msg); + logRescueException(level, t); } } @@ -274,11 +271,31 @@ public class RescueParty { resetAllSettings(context, Settings.RESET_MODE_TRUSTED_DEFAULTS, failedPackage); break; case LEVEL_FACTORY_RESET: - RecoverySystem.rebootPromptAndWipeUserData(context, TAG); + // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog + // when device shutting down. + Runnable runnable = new Runnable() { + @Override + public void run() { + try { + RecoverySystem.rebootPromptAndWipeUserData(context, TAG); + } catch (Throwable t) { + logRescueException(level, t); + } + } + }; + Thread thread = new Thread(runnable); + thread.start(); break; } } + private static void logRescueException(int level, Throwable t) { + final String msg = ExceptionUtils.getCompleteMessage(t); + EventLogTags.writeRescueFailure(level, msg); + logCriticalInfo(Log.ERROR, + "Failed rescue level " + levelToString(level) + ": " + msg); + } + private static int mapRescueLevelToUserImpact(int rescueLevel) { switch(rescueLevel) { case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index 70b639846e1e..26cb208a3d47 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -450,6 +450,14 @@ final class UiModeManagerService extends SystemService { return oldNightMode != mNightMode; } + private static long toMilliSeconds(LocalTime t) { + return t.toNanoOfDay() / 1000; + } + + private static LocalTime fromMilliseconds(long t) { + return LocalTime.ofNanoOfDay(t * 1000); + } + private void registerScreenOffEventLocked() { if (mPowerSave) return; mWaitForScreenOff = true; @@ -1385,8 +1393,11 @@ final class UiModeManagerService extends SystemService { pw.println("UiModeManager service (uimode) commands:"); pw.println(" help"); pw.println(" Print this help text."); - pw.println(" night [yes|no|auto]"); + pw.println(" night [yes|no|auto|custom]"); pw.println(" Set or read night mode."); + pw.println(" time [start|end] <ISO time>"); + pw.println(" Set custom start/end schedule time" + + " (night mode must be set to custom to apply)."); } @Override @@ -1399,6 +1410,8 @@ final class UiModeManagerService extends SystemService { switch (cmd) { case "night": return handleNightMode(); + case "time": + return handleCustomTime(); default: return handleDefaultCommands(cmd); } @@ -1409,6 +1422,34 @@ final class UiModeManagerService extends SystemService { return -1; } + private int handleCustomTime() throws RemoteException { + final String modeStr = getNextArg(); + if (modeStr == null) { + printCustomTime(); + return 0; + } + switch (modeStr) { + case "start": + final String start = getNextArg(); + mInterface.setCustomNightModeStart(toMilliSeconds(LocalTime.parse(start))); + return 0; + case "end": + final String end = getNextArg(); + mInterface.setCustomNightModeEnd(toMilliSeconds(LocalTime.parse(end))); + return 0; + default: + getErrPrintWriter().println("command must be in [start|end]"); + return -1; + } + } + + private void printCustomTime() throws RemoteException { + getOutPrintWriter().println("start " + fromMilliseconds( + mInterface.getCustomNightModeStart()).toString()); + getOutPrintWriter().println("end " + fromMilliseconds( + mInterface.getCustomNightModeEnd()).toString()); + } + private int handleNightMode() throws RemoteException { final PrintWriter err = getErrPrintWriter(); final String modeStr = getNextArg(); @@ -1424,7 +1465,8 @@ final class UiModeManagerService extends SystemService { return 0; } else { err.println("Error: mode must be '" + NIGHT_MODE_STR_YES + "', '" - + NIGHT_MODE_STR_NO + "', or '" + NIGHT_MODE_STR_AUTO + "'"); + + NIGHT_MODE_STR_NO + "', or '" + NIGHT_MODE_STR_AUTO + + "', or '" + NIGHT_MODE_STR_CUSTOM + "'"); return -1; } } diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 70fbca5b4798..7e28e94a17bb 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -170,6 +170,8 @@ public class BiometricService extends SystemService { final String mOpPackageName; // Info to be shown on BiometricDialog when all cookies are returned. final Bundle mBundle; + // Random id associated to this AuthSession + final int mSysUiSessionId; final int mCallingUid; final int mCallingPid; final int mCallingUserId; @@ -203,11 +205,13 @@ public class BiometricService extends SystemService { mClientReceiver = receiver; mOpPackageName = opPackageName; mBundle = bundle; + mSysUiSessionId = mRandom.nextInt(); mCallingUid = callingUid; mCallingPid = callingPid; mCallingUserId = callingUserId; mModality = modality; mRequireConfirmation = requireConfirmation; + Slog.d(TAG, "New AuthSession, mSysUiSessionId: " + mSysUiSessionId); } boolean isCrypto() { @@ -1457,7 +1461,8 @@ public class BiometricService extends SystemService { false /* requireConfirmation */, mCurrentAuthSession.mUserId, mCurrentAuthSession.mOpPackageName, - mCurrentAuthSession.mSessionId); + mCurrentAuthSession.mSessionId, + mCurrentAuthSession.mSysUiSessionId); } else { mPendingAuthSession.mClientReceiver.onError(modality, error, vendorCode); mPendingAuthSession = null; @@ -1680,7 +1685,8 @@ public class BiometricService extends SystemService { mStatusBarService.showAuthenticationDialog(mCurrentAuthSession.mBundle, mInternalReceiver, modality, requireConfirmation, userId, mCurrentAuthSession.mOpPackageName, - mCurrentAuthSession.mSessionId); + mCurrentAuthSession.mSessionId, + mCurrentAuthSession.mSysUiSessionId); } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); } @@ -1766,7 +1772,8 @@ public class BiometricService extends SystemService { false /* requireConfirmation */, mCurrentAuthSession.mUserId, mCurrentAuthSession.mOpPackageName, - sessionId); + sessionId, + mCurrentAuthSession.mSysUiSessionId); } else { mPendingAuthSession.mState = STATE_AUTH_CALLED; for (AuthenticatorWrapper authenticator : mAuthenticators) { diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java index b2bf1fce0d07..ccbe96f30e04 100644 --- a/services/core/java/com/android/server/location/LocationManagerService.java +++ b/services/core/java/com/android/server/location/LocationManagerService.java @@ -2667,8 +2667,7 @@ public class LocationManagerService extends ILocationManager.Stub { mRequestStatistics.statistics); for (Map.Entry<PackageProviderKey, PackageStatistics> entry : sorted.entrySet()) { - PackageProviderKey key = entry.getKey(); - ipw.println(key.mPackageName + ": " + key.mProviderName + ": " + entry.getValue()); + ipw.println(entry.getKey() + ": " + entry.getValue()); } ipw.decreaseIndent(); diff --git a/services/core/java/com/android/server/location/LocationRequestStatistics.java b/services/core/java/com/android/server/location/LocationRequestStatistics.java index dcdf48ba08d2..e629b428d864 100644 --- a/services/core/java/com/android/server/location/LocationRequestStatistics.java +++ b/services/core/java/com/android/server/location/LocationRequestStatistics.java @@ -16,6 +16,7 @@ package com.android.server.location; +import android.annotation.NonNull; import android.annotation.Nullable; import android.os.SystemClock; import android.util.Log; @@ -25,6 +26,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.Objects; @@ -121,14 +123,30 @@ public class LocationRequestStatistics { this.mProviderName = providerName; } + @NonNull + @Override + public String toString() { + return mProviderName + ": " + mPackageName + + (mFeatureId == null ? "" : ": " + mFeatureId); + } + + /** + * Sort by provider, then package, then feature + */ @Override public int compareTo(PackageProviderKey other) { final int providerCompare = mProviderName.compareTo(other.mProviderName); if (providerCompare != 0) { return providerCompare; - } else { - return mProviderName.compareTo(other.mProviderName); } + + final int packageCompare = mPackageName.compareTo(other.mPackageName); + if (packageCompare != 0) { + return packageCompare; + } + + return Objects.compare(mFeatureId, other.mFeatureId, Comparator + .nullsFirst(String::compareTo)); } @Override diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java index a5de90c93aab..a435f1e16b80 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java @@ -315,9 +315,7 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider return; } - sessionInfo = new RoutingSessionInfo.Builder(sessionInfo) - .setProviderId(getUniqueId()) - .build(); + sessionInfo = updateSessionInfo(sessionInfo); boolean duplicateSessionAlreadyExists = false; synchronized (mLock) { @@ -348,9 +346,7 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider return; } - sessionInfo = new RoutingSessionInfo.Builder(sessionInfo) - .setProviderId(getUniqueId()) - .build(); + sessionInfo = updateSessionInfo(sessionInfo); boolean found = false; synchronized (mLock) { @@ -380,9 +376,7 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider return; } - sessionInfo = new RoutingSessionInfo.Builder(sessionInfo) - .setProviderId(getUniqueId()) - .build(); + sessionInfo = updateSessionInfo(sessionInfo); boolean found = false; synchronized (mLock) { @@ -403,6 +397,13 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider mCallback.onSessionReleased(this, sessionInfo); } + private RoutingSessionInfo updateSessionInfo(RoutingSessionInfo sessionInfo) { + return new RoutingSessionInfo.Builder(sessionInfo) + .setOwnerPackageName(mComponentName.getPackageName()) + .setProviderId(getUniqueId()) + .build(); + } + private void onRequestFailed(Connection connection, long requestId, int reason) { if (mActiveConnection != connection) { return; diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java index ec941c8aea59..3283fd9b2c51 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -799,7 +799,6 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { writePolicyAL(); } - enableFirewallChainUL(FIREWALL_CHAIN_STANDBY, true); setRestrictBackgroundUL(mLoadedRestrictBackground, "init_service"); updateRulesForGlobalChangeAL(false); updateNotificationsNL(); @@ -871,6 +870,9 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { new NetworkRequest.Builder().build(), mNetworkCallback); mAppStandby.addListener(new NetPolicyAppIdleStateChangeListener()); + synchronized (mUidRulesFirstLock) { + updateRulesForAppIdleParoleUL(); + } // Listen for subscriber changes mContext.getSystemService(SubscriptionManager.class).addOnSubscriptionsChangedListener( @@ -3893,6 +3895,39 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } /** + * Toggle the firewall standby chain and inform listeners if the uid rules have effectively + * changed. + */ + @GuardedBy("mUidRulesFirstLock") + private void updateRulesForAppIdleParoleUL() { + final boolean paroled = mAppStandby.isInParole(); + final boolean enableChain = !paroled; + enableFirewallChainUL(FIREWALL_CHAIN_STANDBY, enableChain); + + int ruleCount = mUidFirewallStandbyRules.size(); + for (int i = 0; i < ruleCount; i++) { + final int uid = mUidFirewallStandbyRules.keyAt(i); + int oldRules = mUidRules.get(uid); + if (enableChain) { + // Chain wasn't enabled before and the other power-related + // chains are whitelists, so we can clear the + // MASK_ALL_NETWORKS part of the rules and re-inform listeners if + // the effective rules result in blocking network access. + oldRules &= MASK_METERED_NETWORKS; + } else { + // Skip if it had no restrictions to begin with + if ((oldRules & MASK_ALL_NETWORKS) == 0) continue; + } + final int newUidRules = updateRulesForPowerRestrictionsUL(uid, oldRules, paroled); + if (newUidRules == RULE_NONE) { + mUidRules.delete(uid); + } else { + mUidRules.put(uid, newUidRules); + } + } + } + + /** * Update rules that might be changed by {@link #mRestrictBackground}, * {@link #mRestrictPower}, or {@link #mDeviceIdleMode} value. */ @@ -4347,7 +4382,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { private void updateRulesForPowerRestrictionsUL(int uid) { final int oldUidRules = mUidRules.get(uid, RULE_NONE); - final int newUidRules = updateRulesForPowerRestrictionsUL(uid, oldUidRules); + final int newUidRules = updateRulesForPowerRestrictionsUL(uid, oldUidRules, false); if (newUidRules == RULE_NONE) { mUidRules.delete(uid); @@ -4361,28 +4396,30 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { * * @param uid the uid of the app to update rules for * @param oldUidRules the current rules for the uid, in order to determine if there's a change + * @param paroled whether to ignore idle state of apps and only look at other restrictions * * @return the new computed rules for the uid */ - private int updateRulesForPowerRestrictionsUL(int uid, int oldUidRules) { + private int updateRulesForPowerRestrictionsUL(int uid, int oldUidRules, boolean paroled) { if (Trace.isTagEnabled(Trace.TRACE_TAG_NETWORK)) { Trace.traceBegin(Trace.TRACE_TAG_NETWORK, - "updateRulesForPowerRestrictionsUL: " + uid + "/" + oldUidRules); + "updateRulesForPowerRestrictionsUL: " + uid + "/" + oldUidRules + "/" + + (paroled ? "P" : "-")); } try { - return updateRulesForPowerRestrictionsULInner(uid, oldUidRules); + return updateRulesForPowerRestrictionsULInner(uid, oldUidRules, paroled); } finally { Trace.traceEnd(Trace.TRACE_TAG_NETWORK); } } - private int updateRulesForPowerRestrictionsULInner(int uid, int oldUidRules) { + private int updateRulesForPowerRestrictionsULInner(int uid, int oldUidRules, boolean paroled) { if (!isUidValidForBlacklistRules(uid)) { if (LOGD) Slog.d(TAG, "no need to update restrict power rules for uid " + uid); return RULE_NONE; } - final boolean isIdle = isUidIdle(uid); + final boolean isIdle = !paroled && isUidIdle(uid); final boolean restrictMode = isIdle || mRestrictPower || mDeviceIdleMode; final boolean isForeground = isUidForegroundOnRestrictPowerUL(uid); @@ -4452,6 +4489,14 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } catch (NameNotFoundException nnfe) { } } + + @Override + public void onParoleStateChanged(boolean isParoleOn) { + synchronized (mUidRulesFirstLock) { + mLogger.paroleStateChanged(isParoleOn); + updateRulesForAppIdleParoleUL(); + } + } } private void dispatchUidRulesChanged(INetworkPolicyListener listener, int uid, int uidRules) { diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 8e3de1598275..a9fa2b1bd491 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -1382,13 +1382,21 @@ public final class NotificationRecord { */ public boolean isConversation() { Notification notification = getNotification(); - if (mChannel.isDemoted() - || !Notification.MessagingStyle.class.equals(notification.getNotificationStyle())) { + if (!Notification.MessagingStyle.class.equals(notification.getNotificationStyle())) { + // very common; don't bother logging + return false; + } + if (mChannel.isDemoted()) { return false; } if (mIsNotConversationOverride) { return false; } + if (mTargetSdkVersion >= Build.VERSION_CODES.R + && Notification.MessagingStyle.class.equals(notification.getNotificationStyle()) + && mShortcutInfo == null) { + return false; + } return true; } diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 3367cd556b2b..5b5f334803e5 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -676,7 +676,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements session = new PackageInstallerSession(mInternalCallback, mContext, mPm, this, mInstallThread.getLooper(), mStagingManager, sessionId, userId, callingUid, installSource, params, createdMillis, - stageDir, stageCid, null, false, false, false, null, SessionInfo.INVALID_ID, + stageDir, stageCid, null, false, false, false, false, null, SessionInfo.INVALID_ID, false, false, false, SessionInfo.STAGED_SESSION_NO_ERROR, ""); synchronized (mSessions) { @@ -830,7 +830,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements synchronized (mSessions) { final PackageInstallerSession session = mSessions.get(sessionId); - return session != null + return (session != null && !(session.isStaged() && session.isDestroyed())) ? session.generateInfoForCaller(true /*withIcon*/, Binder.getCallingUid()) : null; } @@ -851,7 +851,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements synchronized (mSessions) { for (int i = 0; i < mSessions.size(); i++) { final PackageInstallerSession session = mSessions.valueAt(i); - if (session.userId == userId && !session.hasParentSessionId()) { + if (session.userId == userId && !session.hasParentSessionId() + && !(session.isStaged() && session.isDestroyed())) { result.add(session.generateInfoForCaller(false, callingUid)); } } @@ -1269,7 +1270,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements public void onStagedSessionChanged(PackageInstallerSession session) { session.markUpdated(); writeSessionsAsync(); - if (mOkToSendBroadcasts) { + if (mOkToSendBroadcasts && !session.isDestroyed()) { // we don't scrub the data here as this is sent only to the installer several // privileged system packages mPm.sendSessionUpdatedBroadcast( diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index d3f377e135cb..e0d057a8d7f3 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -190,6 +190,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private static final String ATTR_SESSION_STAGE_CID = "sessionStageCid"; private static final String ATTR_PREPARED = "prepared"; private static final String ATTR_COMMITTED = "committed"; + private static final String ATTR_DESTROYED = "destroyed"; private static final String ATTR_SEALED = "sealed"; private static final String ATTR_MULTI_PACKAGE = "multiPackage"; private static final String ATTR_PARENT_SESSION_ID = "parentSessionId"; @@ -533,7 +534,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { int sessionId, int userId, int installerUid, @NonNull InstallSource installSource, SessionParams params, long createdMillis, File stageDir, String stageCid, InstallationFile[] files, boolean prepared, - boolean committed, boolean sealed, + boolean committed, boolean destroyed, boolean sealed, @Nullable int[] childSessionIds, int parentSessionId, boolean isReady, boolean isFailed, boolean isApplied, int stagedSessionErrorCode, String stagedSessionErrorMessage) { @@ -579,6 +580,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mPrepared = prepared; mCommitted = committed; + mDestroyed = destroyed; mStagedSessionReady = isReady; mStagedSessionFailed = isFailed; mStagedSessionApplied = isApplied; @@ -713,6 +715,13 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + /** {@hide} */ + boolean isDestroyed() { + synchronized (mLock) { + return mDestroyed; + } + } + /** Returns true if a staged session has reached a final state and can be forgotten about */ public boolean isStagedAndInTerminalState() { synchronized (mLock) { @@ -1068,6 +1077,19 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } /** + * Check if the caller is the owner of this session. Otherwise throw a + * {@link SecurityException}. + */ + @GuardedBy("mLock") + private void assertCallerIsOwnerOrRootOrSystemLocked() { + final int callingUid = Binder.getCallingUid(); + if (callingUid != Process.ROOT_UID && callingUid != mInstallerUid + && callingUid != Process.SYSTEM_UID) { + throw new SecurityException("Session does not belong to uid " + callingUid); + } + } + + /** * If anybody is reading or writing data of the session, throw an {@link SecurityException}. */ @GuardedBy("mLock") @@ -2552,7 +2574,13 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { + mParentSessionId + " and may not be abandoned directly."); } synchronized (mLock) { - assertCallerIsOwnerOrRootLocked(); + if (params.isStaged && mDestroyed) { + // If a user abandons staged session in an unsafe state, then system will try to + // abandon the destroyed staged session when it is safe on behalf of the user. + assertCallerIsOwnerOrRootOrSystemLocked(); + } else { + assertCallerIsOwnerOrRootLocked(); + } if (isStagedAndInTerminalState()) { // We keep the session in the database if it's in a finalized state. It will be @@ -2562,11 +2590,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { return; } if (mCommitted && params.isStaged) { - synchronized (mLock) { - mDestroyed = true; + mDestroyed = true; + if (!mStagingManager.abortCommittedSessionLocked(this)) { + // Do not clean up the staged session from system. It is not safe yet. + mCallback.onStagedSessionChanged(this); + return; } - mStagingManager.abortCommittedSession(this); - cleanStageDir(); } @@ -2926,6 +2955,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { /** {@hide} */ void setStagedSessionReady() { synchronized (mLock) { + if (mDestroyed) return; // Do not allow destroyed staged session to change state mStagedSessionReady = true; mStagedSessionApplied = false; mStagedSessionFailed = false; @@ -2939,6 +2969,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { void setStagedSessionFailed(@StagedSessionErrorCode int errorCode, String errorMessage) { synchronized (mLock) { + if (mDestroyed) return; // Do not allow destroyed staged session to change state mStagedSessionReady = false; mStagedSessionApplied = false; mStagedSessionFailed = true; @@ -2953,6 +2984,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { /** {@hide} */ void setStagedSessionApplied() { synchronized (mLock) { + if (mDestroyed) return; // Do not allow destroyed staged session to change state mStagedSessionReady = false; mStagedSessionApplied = true; mStagedSessionFailed = false; @@ -3197,7 +3229,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { */ void write(@NonNull XmlSerializer out, @NonNull File sessionsDir) throws IOException { synchronized (mLock) { - if (mDestroyed) { + if (mDestroyed && !params.isStaged) { return; } @@ -3223,6 +3255,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } writeBooleanAttribute(out, ATTR_PREPARED, isPrepared()); writeBooleanAttribute(out, ATTR_COMMITTED, isCommitted()); + writeBooleanAttribute(out, ATTR_DESTROYED, isDestroyed()); writeBooleanAttribute(out, ATTR_SEALED, isSealed()); writeBooleanAttribute(out, ATTR_MULTI_PACKAGE, params.isMultiPackage); @@ -3352,6 +3385,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { final String stageCid = readStringAttribute(in, ATTR_SESSION_STAGE_CID); final boolean prepared = readBooleanAttribute(in, ATTR_PREPARED, true); final boolean committed = readBooleanAttribute(in, ATTR_COMMITTED); + final boolean destroyed = readBooleanAttribute(in, ATTR_DESTROYED); final boolean sealed = readBooleanAttribute(in, ATTR_SEALED); final int parentSessionId = readIntAttribute(in, ATTR_PARENT_SESSION_ID, SessionInfo.INVALID_ID); @@ -3473,7 +3507,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { return new PackageInstallerSession(callback, context, pm, sessionProvider, installerThread, stagingManager, sessionId, userId, installerUid, installSource, params, createdMillis, stageDir, stageCid, fileArray, - prepared, committed, sealed, childSessionIdsArray, parentSessionId, + prepared, committed, destroyed, sealed, childSessionIdsArray, parentSessionId, isReady, isFailed, isApplied, stagedSessionErrorCode, stagedSessionErrorMessage); } } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index f31dbbf077bb..bde9d5735960 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -5966,25 +5966,7 @@ public class PackageManagerService extends IPackageManager.Stub || shouldFilterApplicationLocked(ps2, callingUid, callingUserId)) { return PackageManager.SIGNATURE_UNKNOWN_PACKAGE; } - SigningDetails p1SigningDetails = p1.getSigningDetails(); - SigningDetails p2SigningDetails = p2.getSigningDetails(); - int result = compareSignatures(p1SigningDetails.signatures, - p2SigningDetails.signatures); - // To support backwards compatibility with clients of this API expecting pre-key - // rotation results if either of the packages has a signing lineage the oldest signer - // in the lineage is used for signature verification. - if (result != PackageManager.SIGNATURE_MATCH && ( - p1SigningDetails.hasPastSigningCertificates() - || p2SigningDetails.hasPastSigningCertificates())) { - Signature[] p1Signatures = p1SigningDetails.hasPastSigningCertificates() - ? new Signature[]{p1SigningDetails.pastSigningCertificates[0]} - : p1SigningDetails.signatures; - Signature[] p2Signatures = p2SigningDetails.hasPastSigningCertificates() - ? new Signature[]{p2SigningDetails.pastSigningCertificates[0]} - : p2SigningDetails.signatures; - result = compareSignatures(p1Signatures, p2Signatures); - } - return result; + return checkSignaturesInternal(p1.getSigningDetails(), p2.getSigningDetails()); } } @@ -5998,21 +5980,21 @@ public class PackageManagerService extends IPackageManager.Stub final int appId2 = UserHandle.getAppId(uid2); // reader synchronized (mLock) { - Signature[] s1; - Signature[] s2; + SigningDetails p1SigningDetails; + SigningDetails p2SigningDetails; Object obj = mSettings.getSettingLPr(appId1); if (obj != null) { if (obj instanceof SharedUserSetting) { if (isCallerInstantApp) { return PackageManager.SIGNATURE_UNKNOWN_PACKAGE; } - s1 = ((SharedUserSetting)obj).signatures.mSigningDetails.signatures; + p1SigningDetails = ((SharedUserSetting) obj).signatures.mSigningDetails; } else if (obj instanceof PackageSetting) { final PackageSetting ps = (PackageSetting) obj; if (shouldFilterApplicationLocked(ps, callingUid, callingUserId)) { return PackageManager.SIGNATURE_UNKNOWN_PACKAGE; } - s1 = ps.signatures.mSigningDetails.signatures; + p1SigningDetails = ps.signatures.mSigningDetails; } else { return PackageManager.SIGNATURE_UNKNOWN_PACKAGE; } @@ -6025,21 +6007,51 @@ public class PackageManagerService extends IPackageManager.Stub if (isCallerInstantApp) { return PackageManager.SIGNATURE_UNKNOWN_PACKAGE; } - s2 = ((SharedUserSetting)obj).signatures.mSigningDetails.signatures; + p2SigningDetails = ((SharedUserSetting) obj).signatures.mSigningDetails; } else if (obj instanceof PackageSetting) { final PackageSetting ps = (PackageSetting) obj; if (shouldFilterApplicationLocked(ps, callingUid, callingUserId)) { return PackageManager.SIGNATURE_UNKNOWN_PACKAGE; } - s2 = ps.signatures.mSigningDetails.signatures; + p2SigningDetails = ps.signatures.mSigningDetails; } else { return PackageManager.SIGNATURE_UNKNOWN_PACKAGE; } } else { return PackageManager.SIGNATURE_UNKNOWN_PACKAGE; } - return compareSignatures(s1, s2); + return checkSignaturesInternal(p1SigningDetails, p2SigningDetails); + } + } + + private int checkSignaturesInternal(SigningDetails p1SigningDetails, + SigningDetails p2SigningDetails) { + if (p1SigningDetails == null) { + return p2SigningDetails == null + ? PackageManager.SIGNATURE_NEITHER_SIGNED + : PackageManager.SIGNATURE_FIRST_NOT_SIGNED; } + if (p2SigningDetails == null) { + return PackageManager.SIGNATURE_SECOND_NOT_SIGNED; + } + int result = compareSignatures(p1SigningDetails.signatures, p2SigningDetails.signatures); + if (result == PackageManager.SIGNATURE_MATCH) { + return result; + } + // To support backwards compatibility with clients of this API expecting pre-key + // rotation results if either of the packages has a signing lineage the oldest signer + // in the lineage is used for signature verification. + if (p1SigningDetails.hasPastSigningCertificates() + || p2SigningDetails.hasPastSigningCertificates()) { + Signature[] p1Signatures = p1SigningDetails.hasPastSigningCertificates() + ? new Signature[]{p1SigningDetails.pastSigningCertificates[0]} + : p1SigningDetails.signatures; + Signature[] p2Signatures = p2SigningDetails.hasPastSigningCertificates() + ? new Signature[]{p2SigningDetails.pastSigningCertificates[0]} + : p2SigningDetails.signatures; + result = compareSignatures(p1Signatures, p2Signatures); + } + return result; } @Override diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java index 9a297d601a6b..a83fa32ec9a9 100644 --- a/services/core/java/com/android/server/pm/StagingManager.java +++ b/services/core/java/com/android/server/pm/StagingManager.java @@ -61,6 +61,7 @@ import android.text.TextUtils; import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.apk.ApkSignatureVerifier; @@ -137,6 +138,9 @@ public class StagingManager { synchronized (mStagedSessions) { for (int i = 0; i < mStagedSessions.size(); i++) { final PackageInstallerSession stagedSession = mStagedSessions.valueAt(i); + if (stagedSession.isDestroyed()) { + continue; + } result.add(stagedSession.generateInfoForCaller(false /*icon*/, callingUid)); } } @@ -202,7 +206,7 @@ public class StagingManager { final IntArray childSessionIds = new IntArray(); if (session.isMultiPackage()) { for (int id : session.getChildSessionIds()) { - if (isApexSession(mStagedSessions.get(id))) { + if (isApexSession(getStagedSession(id))) { childSessionIds.add(id); } } @@ -797,6 +801,8 @@ public class StagingManager { + session.sessionId + " [" + errorMessage + "]"); session.setStagedSessionFailed( SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, errorMessage); + mPreRebootVerificationHandler.onPreRebootVerificationComplete( + session.sessionId); return; } mPreRebootVerificationHandler.notifyPreRebootVerification_Apk_Complete( @@ -880,7 +886,8 @@ public class StagingManager { synchronized (mStagedSessions) { for (int i = 0; i < mStagedSessions.size(); i++) { final PackageInstallerSession stagedSession = mStagedSessions.valueAt(i); - if (!stagedSession.isCommitted() || stagedSession.isStagedAndInTerminalState()) { + if (!stagedSession.isCommitted() || stagedSession.isStagedAndInTerminalState() + || stagedSession.isDestroyed()) { continue; } if (stagedSession.isMultiPackage()) { @@ -943,27 +950,68 @@ public class StagingManager { } } - void abortCommittedSession(@NonNull PackageInstallerSession session) { + /** + * <p>Abort committed staged session + * + * <p>This method must be called while holding {@link PackageInstallerSession.mLock}. + * + * <p>The method returns {@code false} to indicate it is not safe to clean up the session from + * system yet. When it is safe, the method returns {@code true}. + * + * <p> When it is safe to clean up, {@link StagingManager} will call + * {@link PackageInstallerSession#abandon()} on the session again. + * + * @return {@code true} if it is safe to cleanup the session resources, otherwise {@code false}. + */ + boolean abortCommittedSessionLocked(@NonNull PackageInstallerSession session) { + int sessionId = session.sessionId; if (session.isStagedSessionApplied()) { - Slog.w(TAG, "Cannot abort applied session : " + session.sessionId); - return; + Slog.w(TAG, "Cannot abort applied session : " + sessionId); + return false; + } + if (!session.isDestroyed()) { + throw new IllegalStateException("Committed session must be destroyed before aborting it" + + " from StagingManager"); + } + if (getStagedSession(sessionId) == null) { + Slog.w(TAG, "Session " + sessionId + " has been abandoned already"); + return false; } - abortSession(session); - boolean hasApex = sessionContainsApex(session); - if (hasApex) { - ApexSessionInfo apexSession = mApexManager.getStagedSessionInfo(session.sessionId); - if (apexSession == null || isApexSessionFinalized(apexSession)) { - Slog.w(TAG, - "Cannot abort session " + session.sessionId - + " because it is not active or APEXD is not reachable"); - return; - } - try { - mApexManager.abortStagedSession(session.sessionId); - } catch (Exception ignore) { + // If pre-reboot verification is running, then return false. StagingManager will call + // abandon again when pre-reboot verification ends. + if (mPreRebootVerificationHandler.isVerificationRunning(sessionId)) { + Slog.w(TAG, "Session " + sessionId + " aborted before pre-reboot " + + "verification completed."); + return false; + } + + // A session could be marked ready once its pre-reboot verification ends + if (session.isStagedSessionReady()) { + if (sessionContainsApex(session)) { + try { + ApexSessionInfo apexSession = + mApexManager.getStagedSessionInfo(session.sessionId); + if (apexSession == null || isApexSessionFinalized(apexSession)) { + Slog.w(TAG, + "Cannot abort session " + session.sessionId + + " because it is not active."); + } else { + mApexManager.abortStagedSession(session.sessionId); + } + } catch (Exception e) { + // Failed to contact apexd service. The apex might still be staged. We can still + // safely cleanup the staged session since pre-reboot verification is complete. + // Also, cleaning up the stageDir prevents the apex from being activated. + Slog.w(TAG, "Could not contact apexd to abort staged session " + sessionId); + } } } + + // Session was successfully aborted from apexd (if required) and pre-reboot verification + // is also complete. It is now safe to clean up the session from system. + abortSession(session); + return true; } private boolean isApexSessionFinalized(ApexSessionInfo session) { @@ -1042,6 +1090,11 @@ public class StagingManager { // Final states, nothing to do. return; } + if (session.isDestroyed()) { + // Device rebooted before abandoned session was cleaned up. + session.abandon(); + return; + } if (!session.isStagedSessionReady()) { // The framework got restarted before the pre-reboot verification could complete, // restart the verification. @@ -1124,10 +1177,20 @@ public class StagingManager { } } + private PackageInstallerSession getStagedSession(int sessionId) { + PackageInstallerSession session; + synchronized (mStagedSessions) { + session = mStagedSessions.get(sessionId); + } + return session; + } + private final class PreRebootVerificationHandler extends Handler { // Hold session ids before handler gets ready to do the verification. private IntArray mPendingSessionIds; private boolean mIsReady; + @GuardedBy("mVerificationRunning") + private final SparseBooleanArray mVerificationRunning = new SparseBooleanArray(); PreRebootVerificationHandler(Looper looper) { super(looper); @@ -1155,13 +1218,15 @@ public class StagingManager { @Override public void handleMessage(Message msg) { final int sessionId = msg.arg1; - final PackageInstallerSession session; - synchronized (mStagedSessions) { - session = mStagedSessions.get(sessionId); - } - // Maybe session was aborted before pre-reboot verification was complete + final PackageInstallerSession session = getStagedSession(sessionId); if (session == null) { - Slog.d(TAG, "Stopping pre-reboot verification for sessionId: " + sessionId); + Slog.wtf(TAG, "Session disappeared in the middle of pre-reboot verification: " + + sessionId); + return; + } + if (session.isDestroyed()) { + // No point in running verification on a destroyed session + onPreRebootVerificationComplete(sessionId); return; } switch (msg.what) { @@ -1200,9 +1265,40 @@ public class StagingManager { mPendingSessionIds.add(sessionId); return; } + + PackageInstallerSession session = getStagedSession(sessionId); + synchronized (mVerificationRunning) { + // Do not start verification on a session that has been abandoned + if (session == null || session.isDestroyed()) { + return; + } + Slog.d(TAG, "Starting preRebootVerification for session " + sessionId); + mVerificationRunning.put(sessionId, true); + } obtainMessage(MSG_PRE_REBOOT_VERIFICATION_START, sessionId, 0).sendToTarget(); } + // Things to do when pre-reboot verification completes for a particular sessionId + private void onPreRebootVerificationComplete(int sessionId) { + // Remove it from mVerificationRunning so that verification is considered complete + synchronized (mVerificationRunning) { + Slog.d(TAG, "Stopping preRebootVerification for session " + sessionId); + mVerificationRunning.delete(sessionId); + } + // Check if the session was destroyed while pre-reboot verification was running. If so, + // abandon it again. + PackageInstallerSession session = getStagedSession(sessionId); + if (session != null && session.isDestroyed()) { + session.abandon(); + } + } + + private boolean isVerificationRunning(int sessionId) { + synchronized (mVerificationRunning) { + return mVerificationRunning.get(sessionId); + } + } + private void notifyPreRebootVerification_Start_Complete(int sessionId) { obtainMessage(MSG_PRE_REBOOT_VERIFICATION_APEX, sessionId, 0).sendToTarget(); } @@ -1221,8 +1317,6 @@ public class StagingManager { * See {@link PreRebootVerificationHandler} to see all nodes of pre reboot verification */ private void handlePreRebootVerification_Start(@NonNull PackageInstallerSession session) { - Slog.d(TAG, "Starting preRebootVerification for session " + session.sessionId); - if ((session.params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0) { // If rollback is enabled for this session, we call through to the RollbackManager // with the list of sessions it must enable rollback for. Note that @@ -1269,6 +1363,7 @@ public class StagingManager { } } catch (PackageManagerException e) { session.setStagedSessionFailed(e.error, e.getMessage()); + onPreRebootVerificationComplete(session.sessionId); return; } @@ -1301,6 +1396,7 @@ public class StagingManager { // TODO(b/118865310): abort the session on apexd. } catch (PackageManagerException e) { session.setStagedSessionFailed(e.error, e.getMessage()); + onPreRebootVerificationComplete(session.sessionId); } } @@ -1323,9 +1419,18 @@ public class StagingManager { Slog.e(TAG, "Failed to get hold of StorageManager", e); session.setStagedSessionFailed(SessionInfo.STAGED_SESSION_UNKNOWN, "Failed to get hold of StorageManager"); + onPreRebootVerificationComplete(session.sessionId); return; } + // Stop pre-reboot verification before marking session ready. From this point on, if we + // abandon the session then it will be cleaned up immediately. If session is abandoned + // after this point, then even if for some reason system tries to install the session + // or activate its apex, there won't be any files to work with as they will be cleaned + // up by the system as part of abandonment. If session is abandoned before this point, + // then the session is already destroyed and cannot be marked ready anymore. + onPreRebootVerificationComplete(session.sessionId); + // Proactively mark session as ready before calling apexd. Although this call order // looks counter-intuitive, this is the easiest way to ensure that session won't end up // in the inconsistent state: @@ -1337,15 +1442,16 @@ public class StagingManager { // only apex part of the train will be applied, leaving device in an inconsistent state. Slog.d(TAG, "Marking session " + session.sessionId + " as ready"); session.setStagedSessionReady(); - final boolean hasApex = sessionContainsApex(session); - if (!hasApex) { - // Session doesn't contain apex, nothing to do. - return; - } - try { - mApexManager.markStagedSessionReady(session.sessionId); - } catch (PackageManagerException e) { - session.setStagedSessionFailed(e.error, e.getMessage()); + if (session.isStagedSessionReady()) { + final boolean hasApex = sessionContainsApex(session); + if (hasApex) { + try { + mApexManager.markStagedSessionReady(session.sessionId); + } catch (PackageManagerException e) { + session.setStagedSessionFailed(e.error, e.getMessage()); + return; + } + } } } } diff --git a/services/core/java/com/android/server/pm/TEST_MAPPING b/services/core/java/com/android/server/pm/TEST_MAPPING index eb79b6ec652a..d3cd1a90b0b6 100644 --- a/services/core/java/com/android/server/pm/TEST_MAPPING +++ b/services/core/java/com/android/server/pm/TEST_MAPPING @@ -37,6 +37,9 @@ }, { "include-filter": "android.content.pm.cts.PackageManagerShellCommandIncrementalTest" + }, + { + "include-filter": "android.content.pm.cts.PackageManagerTest" } ] }, diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index e6af86e52035..16d96d9a5574 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -4516,8 +4516,8 @@ public class UserManagerService extends IUserManager.Stub { switch(cmd) { case "list": return runList(pw, shell); - case "list-missing-system-packages": - return runListMissingSystemPackages(pw, shell); + case "report-system-user-package-whitelist-problems": + return runReportPackageWhitelistProblems(pw, shell); default: return shell.handleDefaultCommands(cmd); } @@ -4584,17 +4584,22 @@ public class UserManagerService extends IUserManager.Stub { } } - private int runListMissingSystemPackages(PrintWriter pw, Shell shell) { + private int runReportPackageWhitelistProblems(PrintWriter pw, Shell shell) { boolean verbose = false; - boolean force = false; + boolean criticalOnly = false; + int mode = UserSystemPackageInstaller.USER_TYPE_PACKAGE_WHITELIST_MODE_NONE; String opt; while ((opt = shell.getNextOption()) != null) { switch (opt) { case "-v": + case "--verbose": verbose = true; break; - case "--force": - force = true; + case "--critical-only": + criticalOnly = true; + break; + case "--mode": + mode = Integer.parseInt(shell.getNextArgRequired()); break; default: pw.println("Invalid option: " + opt); @@ -4602,8 +4607,12 @@ public class UserManagerService extends IUserManager.Stub { } } + Slog.d(LOG_TAG, "runReportPackageWhitelistProblems(): verbose=" + verbose + + ", criticalOnly=" + criticalOnly + + ", mode=" + UserSystemPackageInstaller.modeToString(mode)); + try (IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ")) { - mSystemPackageInstaller.dumpMissingSystemPackages(ipw, force, verbose); + mSystemPackageInstaller.dumpPackageWhitelistProblems(ipw, mode, verbose, criticalOnly); } return 0; } @@ -5176,13 +5185,18 @@ public class UserManagerService extends IUserManager.Stub { final PrintWriter pw = getOutPrintWriter(); pw.println("User manager (user) commands:"); pw.println(" help"); - pw.println(" Print this help text."); + pw.println(" Prints this help text."); pw.println(""); pw.println(" list [-v] [-all]"); pw.println(" Prints all users on the system."); - pw.println(" list-missing-system-packages [-v] [--force]"); - pw.println(" Prints all system packages that were not explicitly configured to be " - + "installed."); + pw.println(" report-system-user-package-whitelist-problems [-v | --verbose] " + + "[--critical-only] [--mode MODE]"); + pw.println(" Reports all issues on user-type package whitelist XML files. Options:"); + pw.println(" -v | --verbose : shows extra info, like number of issues"); + pw.println(" --critical-only: show only critical issues, excluding warnings"); + pw.println(" --mode MODE: shows what errors would be if device used mode MODE (where" + + " MODE is the whitelist mode integer as defined by " + + "config_userTypePackageWhitelistMode)"); } } diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java index 0b6024a84f78..1fec8aa0a3ff 100644 --- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java +++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java @@ -208,7 +208,6 @@ public class UserRestrictionsUtils { Sets.newArraySet( UserManager.DISALLOW_CONFIG_DATE_TIME, UserManager.DISALLOW_CAMERA, - UserManager.DISALLOW_ADD_USER, UserManager.DISALLOW_BLUETOOTH, UserManager.DISALLOW_BLUETOOTH_SHARING, UserManager.DISALLOW_CONFIG_CELL_BROADCASTS, diff --git a/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java b/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java index cd1087f5fcd7..9ec03e5e8f5c 100644 --- a/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java +++ b/services/core/java/com/android/server/pm/UserSystemPackageInstaller.java @@ -27,7 +27,7 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; -import android.util.Pair; +import android.util.DebugUtils; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; @@ -41,6 +41,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -111,14 +112,20 @@ class UserSystemPackageInstaller { * frameworks/base/core/res/res/values/config.xml */ static final String PACKAGE_WHITELIST_MODE_PROP = "persist.debug.user.package_whitelist_mode"; - static final int USER_TYPE_PACKAGE_WHITELIST_MODE_DISABLE = 0x00; - static final int USER_TYPE_PACKAGE_WHITELIST_MODE_ENFORCE = 0x01; - static final int USER_TYPE_PACKAGE_WHITELIST_MODE_LOG = 0x02; - static final int USER_TYPE_PACKAGE_WHITELIST_MODE_IMPLICIT_WHITELIST = 0x04; - static final int USER_TYPE_PACKAGE_WHITELIST_MODE_IMPLICIT_WHITELIST_SYSTEM = 0x08; - static final int USER_TYPE_PACKAGE_WHITELIST_MODE_IGNORE_OTA = 0x10; + + // NOTE: flags below are public so they can used by DebugUtils.flagsToString. And this class + // itself is package-protected, so it doesn't matter... + public static final int USER_TYPE_PACKAGE_WHITELIST_MODE_DISABLE = 0x00; + public static final int USER_TYPE_PACKAGE_WHITELIST_MODE_ENFORCE = 0x01; + public static final int USER_TYPE_PACKAGE_WHITELIST_MODE_LOG = 0x02; + public static final int USER_TYPE_PACKAGE_WHITELIST_MODE_IMPLICIT_WHITELIST = 0x04; + public static final int USER_TYPE_PACKAGE_WHITELIST_MODE_IMPLICIT_WHITELIST_SYSTEM = 0x08; + public static final int USER_TYPE_PACKAGE_WHITELIST_MODE_IGNORE_OTA = 0x10; static final int USER_TYPE_PACKAGE_WHITELIST_MODE_DEVICE_DEFAULT = -1; + // Used by Shell command only + static final int USER_TYPE_PACKAGE_WHITELIST_MODE_NONE = -1000; + @IntDef(flag = true, prefix = "USER_TYPE_PACKAGE_WHITELIST_MODE_", value = { USER_TYPE_PACKAGE_WHITELIST_MODE_DISABLE, USER_TYPE_PACKAGE_WHITELIST_MODE_ENFORCE, @@ -266,58 +273,56 @@ class UserSystemPackageInstaller { if (!isLogMode(mode) && !isEnforceMode(mode)) { return; } - final List<Pair<Boolean, String>> warnings = checkSystemPackagesWhitelistWarnings(mode); - final int size = warnings.size(); - if (size == 0) { - Slog.v(TAG, "checkWhitelistedSystemPackages(mode=" + mode + "): no warnings"); - return; + Slog.v(TAG, "Checking that all system packages are whitelisted."); + + // Check whether all whitelisted packages are indeed on the system. + final List<String> warnings = getPackagesWhitelistWarnings(); + final int numberWarnings = warnings.size(); + if (numberWarnings == 0) { + Slog.v(TAG, "checkWhitelistedSystemPackages(mode=" + modeToString(mode) + + ") has no warnings"); + } else { + Slog.w(TAG, "checkWhitelistedSystemPackages(mode=" + modeToString(mode) + + ") has " + numberWarnings + " warnings:"); + for (int i = 0; i < numberWarnings; i++) { + Slog.w(TAG, warnings.get(i)); + } } + // Check whether all system packages are indeed whitelisted. if (isImplicitWhitelistMode(mode) && !isLogMode(mode)) { - // Only shows whether all whitelisted packages are indeed on the system. - for (int i = 0; i < size; i++) { - final Pair<Boolean, String> pair = warnings.get(i); - final boolean isSevere = pair.first; - if (!isSevere) { - final String msg = pair.second; - Slog.w(TAG, msg); - } - } return; } - Slog.v(TAG, "checkWhitelistedSystemPackages(mode=" + mode + "): " + size + " warnings"); + final List<String> errors = getPackagesWhitelistErrors(mode); + final int numberErrors = errors.size(); + + if (numberErrors == 0) { + Slog.v(TAG, "checkWhitelistedSystemPackages(mode=" + modeToString(mode) + + ") has no errors"); + return; + } + Slog.e(TAG, "checkWhitelistedSystemPackages(mode=" + modeToString(mode) + ") has " + + numberErrors + " errors:"); + boolean doWtf = !isImplicitWhitelistMode(mode); - for (int i = 0; i < size; i++) { - final Pair<Boolean, String> pair = warnings.get(i); - final boolean isSevere = pair.first; - final String msg = pair.second; - if (isSevere) { - if (doWtf) { - Slog.wtf(TAG, msg); - } else { - Slog.e(TAG, msg); - } + for (int i = 0; i < numberWarnings; i++) { + final String msg = errors.get(i); + if (doWtf) { + Slog.wtf(TAG, msg); } else { - Slog.w(TAG, msg); + Slog.e(TAG, msg); } } } - // TODO: method below was created to refactor the one-time logging logic so it can be used on - // dump / cmd as well. It could to be further refactored (for example, creating a new - // structure for the warnings so it doesn't need a Pair). /** - * Gets warnings for system user whitelisting. - * - * @return list of warnings, where {@code Pair.first} is the severity ({@code true} for WTF, - * {@code false} for WARN) and {@code Pair.second} the message. + * Gets packages that are listed in the whitelist XML but are not present on the system image. */ @NonNull - private List<Pair<Boolean, String>> checkSystemPackagesWhitelistWarnings( - @PackageWhitelistMode int mode) { + private List<String> getPackagesWhitelistWarnings() { final Set<String> allWhitelistedPackages = getWhitelistedSystemPackages(); - final List<Pair<Boolean, String>> warnings = new ArrayList<>(); + final List<String> warnings = new ArrayList<>(); final PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class); // Check whether all whitelisted packages are indeed on the system. @@ -326,25 +331,39 @@ class UserSystemPackageInstaller { for (String pkgName : allWhitelistedPackages) { final AndroidPackage pkg = pmInt.getPackage(pkgName); if (pkg == null) { - warnings.add(new Pair<>(false, String.format(notPresentFmt, pkgName))); + warnings.add(String.format(notPresentFmt, pkgName)); } else if (!pkg.isSystem()) { - warnings.add(new Pair<>(false, String.format(notSystemFmt, pkgName))); + warnings.add(String.format(notSystemFmt, pkgName)); } } + return warnings; + } + + /** + * Gets packages that are not listed in the whitelist XMLs when they should be. + */ + @NonNull + private List<String> getPackagesWhitelistErrors(@PackageWhitelistMode int mode) { + if ((!isEnforceMode(mode) || isImplicitWhitelistMode(mode)) && !isLogMode(mode)) { + return Collections.emptyList(); + } + + final List<String> errors = new ArrayList<>(); + final Set<String> allWhitelistedPackages = getWhitelistedSystemPackages(); + final PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class); // Check whether all system packages are indeed whitelisted. final String logMessageFmt = "System package %s is not whitelisted using " + "'install-in-user-type' in SystemConfig for any user types!"; - final boolean isSevere = isEnforceMode(mode); pmInt.forEachPackage(pkg -> { if (!pkg.isSystem()) return; final String pkgName = pkg.getManifestPackageName(); if (!allWhitelistedPackages.contains(pkgName)) { - warnings.add(new Pair<>(isSevere, String.format(logMessageFmt, pkgName))); + errors.add(String.format(logMessageFmt, pkgName)); } }); - return warnings; + return errors; } /** Whether to only install system packages in new users for which they are whitelisted. */ @@ -420,10 +439,28 @@ class UserSystemPackageInstaller { if (runtimeMode != USER_TYPE_PACKAGE_WHITELIST_MODE_DEVICE_DEFAULT) { return runtimeMode; } + return getDeviceDefaultWhitelistMode(); + } + + /** Gets the PackageWhitelistMode as defined by {@code config_userTypePackageWhitelistMode}. */ + private @PackageWhitelistMode int getDeviceDefaultWhitelistMode() { return Resources.getSystem() .getInteger(com.android.internal.R.integer.config_userTypePackageWhitelistMode); } + static @NonNull String modeToString(@PackageWhitelistMode int mode) { + // Must handle some types separately because they're not bitwise flags + switch (mode) { + case USER_TYPE_PACKAGE_WHITELIST_MODE_DEVICE_DEFAULT: + return "DEVICE_DEFAULT"; + case USER_TYPE_PACKAGE_WHITELIST_MODE_NONE: + return "NONE"; + default: + return DebugUtils.flagsToString(UserSystemPackageInstaller.class, + "USER_TYPE_PACKAGE_WHITELIST_MODE_", mode); + } + } + /** * Gets the system packages names that should be installed on the given user. * See {@link #getInstallablePackagesForUserType(String)}. @@ -703,34 +740,44 @@ class UserSystemPackageInstaller { pw.decreaseIndent(); pw.decreaseIndent(); pw.increaseIndent(); - dumpMissingSystemPackages(pw, /* force= */ true, /* verbose= */ true); + dumpPackageWhitelistProblems(pw, mode, /* verbose= */ true, /* criticalOnly= */ false); pw.decreaseIndent(); } - void dumpMissingSystemPackages(IndentingPrintWriter pw, boolean force, boolean verbose) { - final int mode = getWhitelistMode(); - final boolean show = force || (isEnforceMode(mode) && !isImplicitWhitelistMode(mode)); - if (!show) return; + void dumpPackageWhitelistProblems(IndentingPrintWriter pw, @PackageWhitelistMode int mode, + boolean verbose, boolean criticalOnly) { + // Handle special cases first + if (mode == USER_TYPE_PACKAGE_WHITELIST_MODE_NONE) { + mode = getWhitelistMode(); + } else if (mode == USER_TYPE_PACKAGE_WHITELIST_MODE_DEVICE_DEFAULT) { + mode = getDeviceDefaultWhitelistMode(); + } + Slog.v(TAG, "dumpPackageWhitelistProblems(): using mode " + modeToString(mode)); + + final List<String> errors = getPackagesWhitelistErrors(mode); + showIssues(pw, verbose, errors, "errors"); - final List<Pair<Boolean, String>> warnings = checkSystemPackagesWhitelistWarnings(mode); - final int size = warnings.size(); + if (criticalOnly) return; + final List<String> warnings = getPackagesWhitelistWarnings(); + showIssues(pw, verbose, warnings, "warnings"); + } + + private static void showIssues(IndentingPrintWriter pw, boolean verbose, List<String> issues, + String issueType) { + final int size = issues.size(); if (size == 0) { if (verbose) { - pw.println("All system packages are accounted for"); + pw.print("No "); pw.println(issueType); } return; } - if (verbose) { - pw.print(size); pw.println(" warnings for system user:"); + pw.print(size); pw.print(' '); pw.println(issueType); pw.increaseIndent(); } for (int i = 0; i < size; i++) { - final Pair<Boolean, String> pair = warnings.get(i); - final String lvl = pair.first ? "WTF" : "WARN"; - final String msg = pair.second; - pw.print(lvl); pw.print(": "); pw.println(msg); + pw.println(issues.get(i)); } if (verbose) { pw.decreaseIndent(); diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index f17890334b6d..a967f3d7c4f9 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -149,6 +149,8 @@ import com.android.server.policy.SoftRestrictedPermissionPolicy; import libcore.util.EmptyArray; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -417,6 +419,11 @@ public class PermissionManagerService extends IPermissionManager.Stub { LocalServices.addService(PermissionManagerInternal.class, localService); } + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mContext.getSystemService(PermissionControllerManager.class).dump(fd, pw, args); + } + /** * Creates and returns an initialized, internal service for use by other components. * <p> diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 2f84a99774f4..3bc151af3589 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -1222,10 +1222,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { case LONG_PRESS_POWER_NOTHING: break; case LONG_PRESS_POWER_GLOBAL_ACTIONS: - mPowerKeyHandled = true; - performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, false, - "Power - Long Press - Global Actions"); - showGlobalActionsInternal(); + if (!mPowerKeyHandled) { + mPowerKeyHandled = true; + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, false, + "Power - Long Press - Global Actions"); + showGlobalActionsInternal(); + } break; case LONG_PRESS_POWER_SHUT_OFF: case LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM: diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index b0c702f55821..3b4c4235d8a4 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -30,6 +30,7 @@ import static android.util.MathUtils.abs; import static android.util.MathUtils.constrain; import static com.android.internal.util.FrameworkStatsLog.ANNOTATION_ID_IS_UID; +import static com.android.internal.util.FrameworkStatsLog.ANNOTATION_ID_TRUNCATE_TIMESTAMP; import static com.android.server.am.MemoryStatUtil.readMemoryStatFromFilesystem; import static com.android.server.stats.pull.IonMemoryUtil.readProcessSystemIonHeapSizesFromDebugfs; import static com.android.server.stats.pull.IonMemoryUtil.readSystemIonHeapSizeFromDebugfs; @@ -755,6 +756,13 @@ public class StatsPullAtomService extends SystemService { stats.getValues(j, entry); StatsEvent.Builder e = StatsEvent.newBuilder(); e.setAtomId(atomTag); + switch (atomTag) { + case FrameworkStatsLog.MOBILE_BYTES_TRANSFER: + case FrameworkStatsLog.MOBILE_BYTES_TRANSFER_BY_FG_BG: + e.addBooleanAnnotation(ANNOTATION_ID_TRUNCATE_TIMESTAMP, true); + break; + default: + } e.writeInt(entry.uid); e.addBooleanAnnotation(ANNOTATION_ID_IS_UID, true); if (withFgbg) { diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 289bf66e1add..4ba58bd259fc 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -665,12 +665,12 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D @Override public void showAuthenticationDialog(Bundle bundle, IBiometricServiceReceiverInternal receiver, int biometricModality, boolean requireConfirmation, int userId, String opPackageName, - long operationId) { + long operationId, int sysUiSessionId) { enforceBiometricDialog(); if (mBar != null) { try { mBar.showAuthenticationDialog(bundle, receiver, biometricModality, - requireConfirmation, userId, opPackageName, operationId); + requireConfirmation, userId, opPackageName, operationId, sysUiSessionId); } catch (RemoteException ex) { } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index f05217c0b47a..ad806c2bcae7 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -1160,8 +1160,14 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } else { mLastReportedMultiWindowMode = inMultiWindowMode; computeConfigurationAfterMultiWindowModeChange(); - ensureActivityConfiguration(0 /* globalChanges */, PRESERVE_WINDOWS, - true /* ignoreVisibility */); + // If the activity is in stopping or stopped state, for instance, it's in the + // split screen task and not the top one, the last configuration it should keep + // is the one before multi-window mode change. + final ActivityState state = getState(); + if (state != STOPPED && state != STOPPING) { + ensureActivityConfiguration(0 /* globalChanges */, PRESERVE_WINDOWS, + true /* ignoreVisibility */); + } } } } @@ -1281,12 +1287,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } if (stack != null && stack.topRunningActivity() == this) { - // carry over the PictureInPictureParams to the parent stack without calling - // TaskOrganizerController#dispatchTaskInfoChanged. - // this is to ensure the stack holding up-to-dated pinned stack information - // when activity is re-parented to enter pip mode, see also - // RootWindowContainer#moveActivityToPinnedStack - stack.mPictureInPictureParams.copyOnlySet(pictureInPictureArgs); // make ensure the TaskOrganizer still works after re-parenting if (firstWindowDrawn) { stack.setHasBeenVisible(true); @@ -7769,6 +7769,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A void setPictureInPictureParams(PictureInPictureParams p) { pictureInPictureArgs.copyOnlySet(p); - getTask().getRootTask().setPictureInPictureParams(p); + getTask().getRootTask().onPictureInPictureParamsChanged(); } } diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java index db5e97250cd2..1d7255578759 100644 --- a/services/core/java/com/android/server/wm/ActivityStack.java +++ b/services/core/java/com/android/server/wm/ActivityStack.java @@ -242,12 +242,6 @@ class ActivityStack extends Task { */ boolean mInResumeTopActivity = false; - private boolean mUpdateBoundsDeferred; - private boolean mUpdateBoundsDeferredCalled; - private boolean mUpdateDisplayedBoundsDeferredCalled; - private final Rect mDeferredBounds = new Rect(); - private final Rect mDeferredDisplayedBounds = new Rect(); - int mCurrentUser; /** For comparison with DisplayContent bounds. */ @@ -708,8 +702,10 @@ class ActivityStack extends Task { // Need to make sure windowing mode is supported. If we in the process of creating the stack // no need to resolve the windowing mode again as it is already resolved to the right mode. if (!creating) { - windowingMode = taskDisplayArea.validateWindowingMode(windowingMode, - null /* ActivityRecord */, topTask, getActivityType()); + if (!taskDisplayArea.isValidWindowingMode(windowingMode, null /* ActivityRecord */, + topTask, getActivityType())) { + windowingMode = WINDOWING_MODE_UNDEFINED; + } } if (taskDisplayArea.getRootSplitScreenPrimaryTask() == this && windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) { @@ -846,58 +842,6 @@ class ActivityStack extends Task { return getDisplayContent(); } - /** - * Defers updating the bounds of the stack. If the stack was resized/repositioned while - * deferring, the bounds will update in {@link #continueUpdateBounds()}. - */ - void deferUpdateBounds() { - if (!mUpdateBoundsDeferred) { - mUpdateBoundsDeferred = true; - mUpdateBoundsDeferredCalled = false; - } - } - - /** - * Continues updating bounds after updates have been deferred. If there was a resize attempt - * between {@link #deferUpdateBounds()} and {@link #continueUpdateBounds()}, the stack will - * be resized to that bounds. - */ - void continueUpdateBounds() { - if (mUpdateBoundsDeferred) { - mUpdateBoundsDeferred = false; - if (mUpdateBoundsDeferredCalled) { - setTaskBounds(mDeferredBounds); - setBounds(mDeferredBounds); - } - } - } - - private boolean updateBoundsAllowed(Rect bounds) { - if (!mUpdateBoundsDeferred) { - return true; - } - if (bounds != null) { - mDeferredBounds.set(bounds); - } else { - mDeferredBounds.setEmpty(); - } - mUpdateBoundsDeferredCalled = true; - return false; - } - - private boolean updateDisplayedBoundsAllowed(Rect bounds) { - if (!mUpdateBoundsDeferred) { - return true; - } - if (bounds != null) { - mDeferredDisplayedBounds.set(bounds); - } else { - mDeferredDisplayedBounds.setEmpty(); - } - mUpdateDisplayedBoundsDeferredCalled = true; - return false; - } - /** @return true if the stack can only contain one task */ boolean isSingleTaskInstance() { final DisplayContent display = getDisplay(); @@ -2687,10 +2631,6 @@ class ActivityStack extends Task { // TODO: Can only be called from special methods in ActivityStackSupervisor. // Need to consolidate those calls points into this resize method so anyone can call directly. void resize(Rect displayedBounds, boolean preserveWindows, boolean deferResume) { - if (!updateBoundsAllowed(displayedBounds)) { - return; - } - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "stack.resize_" + getRootTaskId()); mAtmService.deferWindowLayout(); try { @@ -2730,10 +2670,6 @@ class ActivityStack extends Task { * basically resizes both stack and task bounds to the same bounds. */ private void setTaskBounds(Rect bounds) { - if (!updateBoundsAllowed(bounds)) { - return; - } - final PooledConsumer c = PooledLambda.obtainConsumer(ActivityStack::setTaskBounds, PooledLambda.__(Task.class), bounds); forAllLeafTasks(c, true /* traverseTopToBottom */); diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java index ed2153960754..33715207c6ce 100644 --- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java @@ -69,7 +69,6 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLAS import static com.android.server.wm.ActivityTaskManagerService.ANIMATE; import static com.android.server.wm.ActivityTaskManagerService.H.FIRST_SUPERVISOR_STACK_MSG; import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE; -import static com.android.server.wm.RootWindowContainer.MATCH_TASK_IN_STACKS_ONLY; import static com.android.server.wm.RootWindowContainer.MATCH_TASK_IN_STACKS_OR_RECENT_TASKS; import static com.android.server.wm.RootWindowContainer.MATCH_TASK_IN_STACKS_OR_RECENT_TASKS_AND_RESTORE; import static com.android.server.wm.RootWindowContainer.TAG_STATES; @@ -125,7 +124,6 @@ import android.os.UserManager; import android.os.WorkSource; import android.provider.MediaStore; import android.util.ArrayMap; -import android.util.ArraySet; import android.util.MergedConfiguration; import android.util.Slog; import android.util.SparseArray; @@ -364,11 +362,6 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { */ boolean mAppVisibilitiesChangedSinceLastPause; - /** - * Set of tasks that are in resizing mode during an app transition to fill the "void". - */ - private final ArraySet<Integer> mResizingTasksDuringAnimation = new ArraySet<>(); - private KeyguardController mKeyguardController; private PowerManager mPowerManager; @@ -1415,29 +1408,6 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { return mLaunchParamsController; } - private void deferUpdateRecentsHomeStackBounds() { - mRootWindowContainer.deferUpdateBounds(ACTIVITY_TYPE_RECENTS); - mRootWindowContainer.deferUpdateBounds(ACTIVITY_TYPE_HOME); - } - - private void continueUpdateRecentsHomeStackBounds() { - mRootWindowContainer.continueUpdateBounds(ACTIVITY_TYPE_RECENTS); - mRootWindowContainer.continueUpdateBounds(ACTIVITY_TYPE_HOME); - } - - void notifyAppTransitionDone() { - continueUpdateRecentsHomeStackBounds(); - for (int i = mResizingTasksDuringAnimation.size() - 1; i >= 0; i--) { - final int taskId = mResizingTasksDuringAnimation.valueAt(i); - final Task task = - mRootWindowContainer.anyTaskForId(taskId, MATCH_TASK_IN_STACKS_ONLY); - if (task != null) { - task.setTaskDockedResizing(false); - } - } - mResizingTasksDuringAnimation.clear(); - } - void setSplitScreenResizing(boolean resizing) { if (resizing == mDockedStackResizing) { return; @@ -1470,6 +1440,7 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { mService.deferWindowLayout(); try { stack.setWindowingMode(WINDOWING_MODE_UNDEFINED); + stack.setBounds(null); if (toDisplay.getDisplayId() != stack.getDisplayId()) { stack.reparent(toDisplay.getDefaultTaskDisplayArea(), false /* onTop */); } else { @@ -2471,16 +2442,6 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { } } - /** - * Puts a task into resizing mode during the next app transition. - * - * @param task The task to put into resizing mode - */ - void setResizingDuringAnimation(Task task) { - mResizingTasksDuringAnimation.add(task.mTaskId); - task.setTaskDockedResizing(true); - } - int startActivityFromRecents(int callingPid, int callingUid, int taskId, SafeActivityOptions options) { Task task = null; @@ -2508,27 +2469,12 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { mService.deferWindowLayout(); try { - if (windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) { - // Defer updating the stack in which recents is until the app transition is done, to - // not run into issues where we still need to draw the task in recents but the - // docked stack is already created. - deferUpdateRecentsHomeStackBounds(); - // TODO(task-hierarchy): Remove when tiles are in hierarchy. - // Unset launching windowing mode to prevent creating split-screen-primary stack - // in RWC#anyTaskForId() below. - activityOptions.setLaunchWindowingMode(WINDOWING_MODE_UNDEFINED); - } - task = mRootWindowContainer.anyTaskForId(taskId, MATCH_TASK_IN_STACKS_OR_RECENT_TASKS_AND_RESTORE, activityOptions, ON_TOP); if (task == null) { - continueUpdateRecentsHomeStackBounds(); mWindowManager.executeAppTransition(); throw new IllegalArgumentException( "startActivityFromRecents: Task " + taskId + " not found."); - } else if (windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY - && task.getWindowingMode() != windowingMode) { - mService.moveTaskToSplitScreenPrimaryTask(task, true /* toTop */); } if (windowingMode != WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) { @@ -2577,12 +2523,6 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { false /* validateIncomingUser */, null /* originatingPendingIntent */, false /* allowBackgroundActivityStart */); } finally { - if (windowingMode == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY && task != null) { - // If we are launching the task in the docked stack, put it into resizing mode so - // the window renders full-screen with the background filling the void. Also only - // call this at the end to make sure that tasks exists on the window manager side. - setResizingDuringAnimation(task); - } mService.continueWindowLayout(); } } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index 4181f4be30f7..d5df9068e81d 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -152,17 +152,6 @@ public abstract class ActivityTaskManagerInternal { IVoiceInteractor mInteractor); /** - * Callback for window manager to let activity manager know that the app transition was - * cancelled. - */ - public abstract void notifyAppTransitionCancelled(); - - /** - * Callback for window manager to let activity manager know that the app transition is finished. - */ - public abstract void notifyAppTransitionFinished(); - - /** * Returns the top activity from each of the currently visible stacks. The first entry will be * the focused activity. */ diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 6a8d5d905a00..36caeecbfec2 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -6079,20 +6079,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override - public void notifyAppTransitionFinished() { - synchronized (mGlobalLock) { - mStackSupervisor.notifyAppTransitionDone(); - } - } - - @Override - public void notifyAppTransitionCancelled() { - synchronized (mGlobalLock) { - mStackSupervisor.notifyAppTransitionDone(); - } - } - - @Override public List<IBinder> getTopVisibleActivities() { synchronized (mGlobalLock) { return mRootWindowContainer.getTopVisibleActivities(); diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 8b9e9fe132b7..c93b7354999b 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2170,7 +2170,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> final boolean singleActivity = task.getChildCount() == 1; final ActivityStack stack; if (singleActivity) { - stack = r.getRootTask(); + stack = (ActivityStack) task; } else { // In the case of multiple activities, we will create a new task for it and then // move the PIP activity into the task. @@ -2183,6 +2183,11 @@ class RootWindowContainer extends WindowContainer<DisplayContent> // up-to-dated pinned stack information on this newly created stack. r.reparent(stack, MAX_VALUE, reason); } + if (stack.getParent() != taskDisplayArea) { + // stack is nested, but pinned tasks need to be direct children of their + // display area, so reparent. + stack.reparent(taskDisplayArea, true /* onTop */); + } stack.setWindowingMode(WINDOWING_MODE_PINNED); // Reset the state that indicates it can enter PiP while pausing after we've moved it @@ -2504,20 +2509,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> return list; } - void deferUpdateBounds(int activityType) { - final ActivityStack stack = getStack(WINDOWING_MODE_UNDEFINED, activityType); - if (stack != null) { - stack.deferUpdateBounds(); - } - } - - void continueUpdateBounds(int activityType) { - final ActivityStack stack = getStack(WINDOWING_MODE_UNDEFINED, activityType); - if (stack != null) { - stack.continueUpdateBounds(); - } - } - @Override public void onDisplayAdded(int displayId) { if (DEBUG_STACK) Slog.v(TAG, "Display added displayId=" + displayId); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 44a8daaba1b1..85a31610964e 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -82,7 +82,6 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_TASKS import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.ActivityTaskManagerService.TAG_STACK; -import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_DOCKED_DIVIDER; import static com.android.server.wm.IdentifierProto.HASH_CODE; import static com.android.server.wm.IdentifierProto.TITLE; import static com.android.server.wm.IdentifierProto.USER_ID; @@ -110,7 +109,6 @@ import android.app.ActivityManager.TaskSnapshot; import android.app.ActivityOptions; import android.app.ActivityTaskManager; import android.app.AppGlobals; -import android.app.PictureInPictureParams; import android.app.TaskInfo; import android.app.WindowConfiguration; import android.content.ComponentName; @@ -491,12 +489,6 @@ class Task extends WindowContainer<WindowContainer> { boolean mTaskAppearedSent; /** - * Last Picture-in-Picture params applicable to the task. Updated when the app - * enters Picture-in-Picture or when setPictureInPictureParams is called. - */ - PictureInPictureParams mPictureInPictureParams = new PictureInPictureParams.Builder().build(); - - /** * This task was created by the task organizer which has the following implementations. * <ul> * <lis>The task won't be removed when it is empty. Removal has to be an explicit request @@ -2013,7 +2005,7 @@ class Task extends WindowContainer<WindowContainer> { } void updateSurfaceSize(SurfaceControl.Transaction transaction) { - if (mSurfaceControl == null || mCreatedByOrganizer) { + if (mSurfaceControl == null || isOrganized()) { return; } @@ -3059,15 +3051,6 @@ class Task extends WindowContainer<WindowContainer> { return mDragResizeMode; } - /** - * Puts this task into docked drag resizing mode. See {@link DragResizeMode}. - * - * @param resizing Whether to put the task into drag resize mode. - */ - public void setTaskDockedResizing(boolean resizing) { - setDragResizing(resizing, DRAG_RESIZE_MODE_DOCKED_DIVIDER); - } - void adjustBoundsForDisplayChangeIfNeeded(final DisplayContent displayContent) { if (displayContent == null) { return; @@ -3602,10 +3585,11 @@ class Task extends WindowContainer<WindowContainer> { info.resizeMode = top != null ? top.mResizeMode : mResizeMode; info.topActivityType = top.getActivityType(); - if (mPictureInPictureParams.empty()) { + ActivityRecord rootActivity = top.getRootActivity(); + if (rootActivity == null || rootActivity.pictureInPictureArgs.empty()) { info.pictureInPictureParams = null; } else { - info.pictureInPictureParams = mPictureInPictureParams; + info.pictureInPictureParams = rootActivity.pictureInPictureArgs; } info.topActivityInfo = mReuseActivitiesReport.top != null ? mReuseActivitiesReport.top.info @@ -4521,8 +4505,7 @@ class Task extends WindowContainer<WindowContainer> { updateShadowsRadius(hasFocus, getPendingTransaction()); } - void setPictureInPictureParams(PictureInPictureParams p) { - mPictureInPictureParams.copyOnlySet(p); + void onPictureInPictureParamsChanged() { if (isOrganized()) { mAtmService.mTaskOrganizerController.dispatchTaskInfoChanged(this, true /* force */); } diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 0a1ee2b79711..37a4c1f6849b 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -21,7 +21,6 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; @@ -1333,16 +1332,16 @@ final class TaskDisplayArea extends DisplayArea<ActivityStack> { } /** - * Check that the requested windowing-mode is appropriate for the specified task and/or activity + * Check if the requested windowing-mode is appropriate for the specified task and/or activity * on this display. * * @param windowingMode The windowing-mode to validate. * @param r The {@link ActivityRecord} to check against. * @param task The {@link Task} to check against. * @param activityType An activity type. - * @return The provided windowingMode or the closest valid mode which is appropriate. + * @return {@code true} if windowingMode is valid, {@code false} otherwise. */ - int validateWindowingMode(int windowingMode, @Nullable ActivityRecord r, @Nullable Task task, + boolean isValidWindowingMode(int windowingMode, @Nullable ActivityRecord r, @Nullable Task task, int activityType) { // Make sure the windowing mode we are trying to use makes sense for what is supported. boolean supportsMultiWindow = mAtmService.mSupportsMultiWindow; @@ -1362,24 +1361,35 @@ final class TaskDisplayArea extends DisplayArea<ActivityStack> { } } + return windowingMode != WINDOWING_MODE_UNDEFINED + && isWindowingModeSupported(windowingMode, supportsMultiWindow, supportsSplitScreen, + supportsFreeform, supportsPip, activityType); + } + + /** + * Check that the requested windowing-mode is appropriate for the specified task and/or activity + * on this display. + * + * @param windowingMode The windowing-mode to validate. + * @param r The {@link ActivityRecord} to check against. + * @param task The {@link Task} to check against. + * @param activityType An activity type. + * @return The provided windowingMode or the closest valid mode which is appropriate. + */ + int validateWindowingMode(int windowingMode, @Nullable ActivityRecord r, @Nullable Task task, + int activityType) { final boolean inSplitScreenMode = isSplitScreenModeActivated(); - if (!inSplitScreenMode - && windowingMode == WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY) { + if (!inSplitScreenMode && windowingMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY) { // Switch to the display's windowing mode if we are not in split-screen mode and we are // trying to launch in split-screen secondary. windowingMode = WINDOWING_MODE_UNDEFINED; - } else if (inSplitScreenMode && (windowingMode == WINDOWING_MODE_FULLSCREEN - || windowingMode == WINDOWING_MODE_UNDEFINED) - && supportsSplitScreen) { + } else if (inSplitScreenMode && windowingMode == WINDOWING_MODE_UNDEFINED) { windowingMode = WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; } - - if (windowingMode != WINDOWING_MODE_UNDEFINED - && isWindowingModeSupported(windowingMode, supportsMultiWindow, supportsSplitScreen, - supportsFreeform, supportsPip, activityType)) { - return windowingMode; + if (!isValidWindowingMode(windowingMode, r, task, activityType)) { + return WINDOWING_MODE_UNDEFINED; } - return WINDOWING_MODE_UNDEFINED; + return windowingMode; } boolean isTopStack(ActivityStack stack) { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 73126c8b6b6a..ae0c9b15689a 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -1064,12 +1064,10 @@ public class WindowManagerService extends IWindowManager.Stub @Override public void onAppTransitionCancelledLocked(int transit) { - mAtmInternal.notifyAppTransitionCancelled(); } @Override public void onAppTransitionFinishedLocked(IBinder token) { - mAtmInternal.notifyAppTransitionFinished(); final ActivityRecord atoken = mRoot.getActivityRecord(token); if (atoken == null) { return; diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index 0e83beed6b90..c570cf1d949f 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -846,6 +846,23 @@ class WindowStateAnimator { } } + private boolean shouldConsumeMainWindowSizeTransaction() { + // We only consume the transaction when the client is calling relayout + // because this is the only time we know the frameNumber will be valid + // due to the client renderer being paused. Put otherwise, only when + // mInRelayout is true can we guarantee the next frame will contain + // the most recent configuration. + if (!mWin.mInRelayout) return false; + // Since we can only do this for one window, we focus on the main application window + if (mAttrType != TYPE_BASE_APPLICATION) return false; + final Task task = mWin.getTask(); + if (task == null) return false; + if (task.getMainWindowSizeChangeTransaction() == null) return false; + // Likewise we only focus on the task root, since we can only use one window + if (!mWin.mActivityRecord.isRootOfTask()) return false; + return true; + } + void setSurfaceBoundariesLocked(final boolean recoveringMemory) { if (mSurfaceController == null) { return; @@ -886,8 +903,9 @@ class WindowStateAnimator { clipRect = mTmpClipRect; } - if (w.mInRelayout && (mAttrType == TYPE_BASE_APPLICATION) && (task != null) - && (task.getMainWindowSizeChangeTransaction() != null)) { + if (shouldConsumeMainWindowSizeTransaction()) { + task.getSurfaceControl().deferTransactionUntil(mWin.getClientViewRootSurface(), + mWin.getFrameNumber()); mSurfaceController.deferTransactionUntil(mWin.getClientViewRootSurface(), mWin.getFrameNumber()); SurfaceControl.mergeToGlobalTransaction(task.getMainWindowSizeChangeTransaction()); diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 9bc5d34c11af..20139451e4b9 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -237,27 +237,28 @@ public: /* --- InputDispatcherPolicyInterface implementation --- */ virtual void notifySwitch(nsecs_t when, uint32_t switchValues, uint32_t switchMask, - uint32_t policyFlags); + uint32_t policyFlags) override; virtual void notifyConfigurationChanged(nsecs_t when); - virtual nsecs_t notifyANR(const sp<InputApplicationHandle>& inputApplicationHandle, - const sp<IBinder>& token, - const std::string& reason); + virtual nsecs_t notifyAnr(const sp<InputApplicationHandle>& inputApplicationHandle, + const sp<IBinder>& token, const std::string& reason) override; virtual void notifyInputChannelBroken(const sp<IBinder>& token); - virtual void notifyFocusChanged(const sp<IBinder>& oldToken, const sp<IBinder>& newToken); - virtual bool filterInputEvent(const InputEvent* inputEvent, uint32_t policyFlags); - virtual void getDispatcherConfiguration(InputDispatcherConfiguration* outConfig); - virtual void interceptKeyBeforeQueueing(const KeyEvent* keyEvent, uint32_t& policyFlags); + virtual void notifyFocusChanged(const sp<IBinder>& oldToken, + const sp<IBinder>& newToken) override; + virtual bool filterInputEvent(const InputEvent* inputEvent, uint32_t policyFlags) override; + virtual void getDispatcherConfiguration(InputDispatcherConfiguration* outConfig) override; + virtual void interceptKeyBeforeQueueing(const KeyEvent* keyEvent, + uint32_t& policyFlags) override; virtual void interceptMotionBeforeQueueing(const int32_t displayId, nsecs_t when, - uint32_t& policyFlags); - virtual nsecs_t interceptKeyBeforeDispatching( - const sp<IBinder>& token, - const KeyEvent* keyEvent, uint32_t policyFlags); - virtual bool dispatchUnhandledKey(const sp<IBinder>& token, - const KeyEvent* keyEvent, uint32_t policyFlags, KeyEvent* outFallbackKeyEvent); - virtual void pokeUserActivity(nsecs_t eventTime, int32_t eventType); - virtual bool checkInjectEventsPermissionNonReentrant( - int32_t injectorPid, int32_t injectorUid); - virtual void onPointerDownOutsideFocus(const sp<IBinder>& touchedToken); + uint32_t& policyFlags) override; + virtual nsecs_t interceptKeyBeforeDispatching(const sp<IBinder>& token, + const KeyEvent* keyEvent, + uint32_t policyFlags) override; + virtual bool dispatchUnhandledKey(const sp<IBinder>& token, const KeyEvent* keyEvent, + uint32_t policyFlags, KeyEvent* outFallbackKeyEvent) override; + virtual void pokeUserActivity(nsecs_t eventTime, int32_t eventType) override; + virtual bool checkInjectEventsPermissionNonReentrant(int32_t injectorPid, + int32_t injectorUid) override; + virtual void onPointerDownOutsideFocus(const sp<IBinder>& touchedToken) override; /* --- PointerControllerPolicyInterface implementation --- */ @@ -692,9 +693,8 @@ static jobject getInputApplicationHandleObjLocalRef(JNIEnv* env, return handle->getInputApplicationHandleObjLocalRef(env); } - -nsecs_t NativeInputManager::notifyANR(const sp<InputApplicationHandle>& inputApplicationHandle, - const sp<IBinder>& token, const std::string& reason) { +nsecs_t NativeInputManager::notifyAnr(const sp<InputApplicationHandle>& inputApplicationHandle, + const sp<IBinder>& token, const std::string& reason) { #if DEBUG_INPUT_DISPATCHER_POLICY ALOGD("notifyANR"); #endif @@ -1453,9 +1453,13 @@ static jint nativeInjectInputEvent(JNIEnv* env, jclass /* clazz */, return INPUT_EVENT_INJECTION_FAILED; } - return (jint) im->getInputManager()->getDispatcher()->injectInputEvent( - & keyEvent, injectorPid, injectorUid, syncMode, timeoutMillis, - uint32_t(policyFlags)); + const int32_t result = + im->getInputManager()->getDispatcher()->injectInputEvent(&keyEvent, injectorPid, + injectorUid, syncMode, + std::chrono::milliseconds( + timeoutMillis), + uint32_t(policyFlags)); + return static_cast<jint>(result); } else if (env->IsInstanceOf(inputEventObj, gMotionEventClassInfo.clazz)) { const MotionEvent* motionEvent = android_view_MotionEvent_getNativePtr(env, inputEventObj); if (!motionEvent) { @@ -1463,9 +1467,13 @@ static jint nativeInjectInputEvent(JNIEnv* env, jclass /* clazz */, return INPUT_EVENT_INJECTION_FAILED; } - return (jint) im->getInputManager()->getDispatcher()->injectInputEvent( - motionEvent, injectorPid, injectorUid, syncMode, timeoutMillis, - uint32_t(policyFlags)); + const int32_t result = + (jint)im->getInputManager() + ->getDispatcher() + ->injectInputEvent(motionEvent, injectorPid, injectorUid, syncMode, + std::chrono::milliseconds(timeoutMillis), + uint32_t(policyFlags)); + return static_cast<jint>(result); } else { jniThrowRuntimeException(env, "Invalid input event type."); return INPUT_EVENT_INJECTION_FAILED; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 3323fa4b53e3..966694ad346c 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -4567,9 +4567,11 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } if (isProfileOwner(adminReceiver, userHandle)) { if (isProfileOwnerOfOrganizationOwnedDevice(userHandle)) { + UserHandle parentUserHandle = UserHandle.of(getProfileParentId(userHandle)); mUserManager.setUserRestriction(UserManager.DISALLOW_REMOVE_MANAGED_PROFILE, - false, - UserHandle.of(getProfileParentId(userHandle))); + false, parentUserHandle); + mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_USER, + false, parentUserHandle); } final ActiveAdmin admin = getActiveAdminUncheckedLocked(adminReceiver, userHandle, /* parent */ false); @@ -7213,6 +7215,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { mUserManager.setUserRestriction( UserManager.DISALLOW_REMOVE_MANAGED_PROFILE, false, UserHandle.SYSTEM); + mUserManager.setUserRestriction( + UserManager.DISALLOW_ADD_USER, false, UserHandle.SYSTEM); // Device-wide policies set by the profile owner need to be cleaned up here. mLockPatternUtils.setDeviceOwnerInfo(null); @@ -13825,6 +13829,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { mUserManager.setUserRestriction(UserManager.DISALLOW_REMOVE_MANAGED_PROFILE, true, parentUser); + mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_USER, true, + parentUser); }); // markProfileOwnerOfOrganizationOwnedDevice will trigger writing of the profile owner diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 0fc333f4b38c..fa3f33067e8e 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1891,6 +1891,10 @@ public final class SystemServer { || mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { t.traceBegin("StartTvInputManager"); mSystemServiceManager.startService(TvInputManagerService.class); + t.traceEnd(); + } + + if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_TUNER)) { t.traceBegin("StartTunerResourceManager"); mSystemServiceManager.startService(TunerResourceManagerService.class); t.traceEnd(); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java index 2ce70b6f0889..b6cf2785d771 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java @@ -129,8 +129,11 @@ public class AccessibilityServiceConnectionTest { public void bind_requestsContextToBindService() { mConnection.bindLocked(); verify(mMockContext).bindServiceAsUser(any(Intent.class), eq(mConnection), - eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE - | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS), any(UserHandle.class)); + eq(Context.BIND_AUTO_CREATE + | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE + | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS + | Context.BIND_INCLUDE_CAPABILITIES), + any(UserHandle.class)); } @Test diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index d2925263125d..285caf34ae67 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -186,7 +186,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + anyInt() /* sysUiSessionId */); } @Test @@ -269,7 +270,8 @@ public class BiometricServiceTest { eq(false) /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + anyInt() /* sysUiSessionId */); } @Test @@ -407,7 +409,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + anyInt() /* sysUiSessionId */); // Hardware authenticated final byte[] HAT = generateRandomHAT(); @@ -462,7 +465,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + anyInt() /* sysUiSessionId */); } @Test @@ -610,7 +614,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, anyString(), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + anyInt() /* sysUiSessionId */); } @Test @@ -711,7 +716,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + anyInt() /* sysUiSessionId */); } @Test @@ -1207,7 +1213,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + anyInt() /* sysUiSessionId */); // Requesting strong and credential, when credential is setup resetReceiver(); @@ -1227,7 +1234,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + anyInt() /* sysUiSessionId */); // Un-downgrading the authenticator allows successful strong auth for (BiometricService.AuthenticatorWrapper wrapper : mBiometricService.mAuthenticators) { @@ -1250,7 +1258,8 @@ public class BiometricServiceTest { anyBoolean() /* requireConfirmation */, anyInt() /* userId */, eq(TEST_PACKAGE_NAME), - anyLong() /* sessionId */); + anyLong() /* sessionId */, + anyInt() /* sysUiSessionId */); } @Test(expected = IllegalStateException.class) diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 6b36bc591b78..ed40fe756ea1 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -1998,7 +1998,6 @@ public class DevicePolicyManagerTest extends DpmTestBase { private static final Set<String> PROFILE_OWNER_ORGANIZATION_OWNED_GLOBAL_RESTRICTIONS = Sets.newSet( UserManager.DISALLOW_CONFIG_DATE_TIME, - UserManager.DISALLOW_ADD_USER, UserManager.DISALLOW_BLUETOOTH_SHARING, UserManager.DISALLOW_CONFIG_CELL_BROADCASTS, UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS, @@ -4005,6 +4004,12 @@ public class DevicePolicyManagerTest extends DpmTestBase { // Any caller should be able to call this method. assertFalse(dpm.isOrganizationOwnedDeviceWithManagedProfile()); configureProfileOwnerOfOrgOwnedDevice(admin1, CALLER_USER_HANDLE); + + verify(getServices().userManager).setUserRestriction( + eq(UserManager.DISALLOW_ADD_USER), + eq(true), + eq(UserHandle.of(UserHandle.USER_SYSTEM))); + assertTrue(dpm.isOrganizationOwnedDeviceWithManagedProfile()); // A random caller from another user should also be able to get the right result. @@ -4012,6 +4017,35 @@ public class DevicePolicyManagerTest extends DpmTestBase { assertTrue(dpm.isOrganizationOwnedDeviceWithManagedProfile()); } + public void testMarkOrganizationOwnedDevice_baseRestrictionsAdded() throws Exception { + addManagedProfile(admin1, DpmMockContext.CALLER_UID, admin1); + + configureProfileOwnerOfOrgOwnedDevice(admin1, CALLER_USER_HANDLE); + + // Base restriction DISALLOW_REMOVE_MANAGED_PROFILE added + verify(getServices().userManager).setUserRestriction( + eq(UserManager.DISALLOW_REMOVE_MANAGED_PROFILE), + eq(true), + eq(UserHandle.of(UserHandle.USER_SYSTEM))); + + // Base restriction DISALLOW_ADD_USER added + verify(getServices().userManager).setUserRestriction( + eq(UserManager.DISALLOW_ADD_USER), + eq(true), + eq(UserHandle.of(UserHandle.USER_SYSTEM))); + + // Assert base restrictions cannot be added or removed by admin + assertExpectException(SecurityException.class, null, () -> + parentDpm.addUserRestriction(admin1, UserManager.DISALLOW_REMOVE_MANAGED_PROFILE)); + assertExpectException(SecurityException.class, null, () -> + parentDpm.clearUserRestriction(admin1, + UserManager.DISALLOW_REMOVE_MANAGED_PROFILE)); + assertExpectException(SecurityException.class, null, () -> + parentDpm.addUserRestriction(admin1, UserManager.DISALLOW_ADD_USER)); + assertExpectException(SecurityException.class, null, () -> + parentDpm.clearUserRestriction(admin1, UserManager.DISALLOW_ADD_USER)); + } + public void testSetTime() throws Exception { mContext.binder.callingUid = DpmMockContext.CALLER_SYSTEM_USER_UID; setupDeviceOwner(); diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java b/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java index d4edab44bae3..63d797e9b95c 100644 --- a/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/PackageInstallerSessionTest.java @@ -178,6 +178,7 @@ public class PackageInstallerSessionTest { /* files */ null, /* prepared */ true, /* committed */ true, + /* destroyed */ staged ? true : false, /* sealed */ false, // Setting to true would trigger some PM logic. /* childSessionIds */ childSessionIds != null ? childSessionIds : new int[0], /* parentSessionId */ parentSessionId, diff --git a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java index 39062f017a73..6718db768fdb 100644 --- a/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java +++ b/services/tests/servicestests/src/com/android/server/usage/AppStandbyControllerTests.java @@ -366,29 +366,87 @@ public class AppStandbyControllerTests { mInjector.mElapsedRealtime, false)); } + private static class TestParoleListener extends AppIdleStateChangeListener { + private boolean mIsParoleOn = false; + private CountDownLatch mLatch; + private boolean mIsExpecting = false; + private boolean mExpectedParoleState; + + boolean getParoleState() { + synchronized (this) { + return mIsParoleOn; + } + } + + void rearmLatch(boolean expectedParoleState) { + synchronized (this) { + mLatch = new CountDownLatch(1); + mIsExpecting = true; + mExpectedParoleState = expectedParoleState; + } + } + + void awaitOnLatch(long time) throws Exception { + mLatch.await(time, TimeUnit.MILLISECONDS); + } + + @Override + public void onAppIdleStateChanged(String packageName, int userId, boolean idle, + int bucket, int reason) { + } + + @Override + public void onParoleStateChanged(boolean isParoleOn) { + synchronized (this) { + // Only record information if it is being looked for + if (mLatch != null && mLatch.getCount() > 0) { + mIsParoleOn = isParoleOn; + if (mIsExpecting && isParoleOn == mExpectedParoleState) { + mLatch.countDown(); + } + } + } + } + } + @Test public void testIsAppIdle_Charging() throws Exception { + TestParoleListener paroleListener = new TestParoleListener(); + mController.addListener(paroleListener); + setChargingState(mController, false); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM); assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); + assertFalse(mController.isInParole()); + paroleListener.rearmLatch(true); setChargingState(mController, true); + paroleListener.awaitOnLatch(2000); + assertTrue(paroleListener.getParoleState()); assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); + assertTrue(mController.isInParole()); + paroleListener.rearmLatch(false); setChargingState(mController, false); + paroleListener.awaitOnLatch(2000); + assertFalse(paroleListener.getParoleState()); assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); + assertFalse(mController.isInParole()); } @Test public void testIsAppIdle_Enabled() throws Exception { setChargingState(mController, false); + TestParoleListener paroleListener = new TestParoleListener(); + mController.addListener(paroleListener); + setAppIdleEnabled(mController, true); mController.setAppStandbyBucket(PACKAGE_1, USER_ID, STANDBY_BUCKET_RARE, REASON_MAIN_FORCED_BY_SYSTEM); @@ -396,11 +454,17 @@ public class AppStandbyControllerTests { assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); + paroleListener.rearmLatch(false); setAppIdleEnabled(mController, false); + paroleListener.awaitOnLatch(2000); + assertTrue(paroleListener.mIsParoleOn); assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); assertFalse(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); + paroleListener.rearmLatch(true); setAppIdleEnabled(mController, true); + paroleListener.awaitOnLatch(2000); + assertFalse(paroleListener.getParoleState()); assertEquals(STANDBY_BUCKET_RARE, getStandbyBucket(mController, PACKAGE_1)); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, 0)); assertTrue(mController.isAppIdleFiltered(PACKAGE_1, UID_1, USER_ID, false)); diff --git a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java index 3c2d55058c3e..182bf949af1f 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java +++ b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java @@ -41,6 +41,7 @@ public class UiServiceTestCase { protected static final String PKG_N_MR1 = "com.example.n_mr1"; protected static final String PKG_O = "com.example.o"; protected static final String PKG_P = "com.example.p"; + protected static final String PKG_R = "com.example.r"; @Rule public final TestableContext mContext = @@ -69,6 +70,8 @@ public class UiServiceTestCase { return Build.VERSION_CODES.O; case PKG_P: return Build.VERSION_CODES.P; + case PKG_R: + return Build.VERSION_CODES.R; default: return Build.VERSION_CODES.CUR_DEVELOPMENT; } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index babe80e4b612..289933e5ecb2 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -250,6 +250,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { private static final int NOTIFICATION_LOCATION_UNKNOWN = 0; + private static final String VALID_CONVO_SHORTCUT_ID = "shortcut"; + @Mock private NotificationListeners mListeners; @Mock private NotificationAssistants mAssistants; @@ -471,6 +473,19 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mShortcutHelper.setLauncherApps(mLauncherApps); mShortcutHelper.setShortcutServiceInternal(mShortcutServiceInternal); + // Pretend the shortcut exists + List<ShortcutInfo> shortcutInfos = new ArrayList<>(); + ShortcutInfo info = mock(ShortcutInfo.class); + when(info.getPackage()).thenReturn(PKG); + when(info.getId()).thenReturn(VALID_CONVO_SHORTCUT_ID); + when(info.getUserId()).thenReturn(USER_SYSTEM); + when(info.isLongLived()).thenReturn(true); + when(info.isEnabled()).thenReturn(true); + shortcutInfos.add(info); + when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcutInfos); + when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(), + anyString(), anyInt(), any())).thenReturn(true); + // Set the testable bubble extractor RankingHelper rankingHelper = mService.getRankingHelper(); BubbleExtractor extractor = rankingHelper.findExtractor(BubbleExtractor.class); @@ -704,6 +719,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { ) .setActions(replyAction) .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setShortcutId(VALID_CONVO_SHORTCUT_ID) .setGroupSummary(isSummary); if (groupKey != null) { nb.setGroup(groupKey); @@ -6100,7 +6116,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testNotificationBubbles_flagRemoved_whenShortcutRemoved() throws RemoteException { - final String shortcutId = "someshortcutId"; setUpPrefsForBubbles(PKG, mUid, true /* global */, BUBBLE_PREFERENCE_ALL /* app */, @@ -6111,27 +6126,16 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Messaging notification with shortcut info Notification.BubbleMetadata metadata = - new Notification.BubbleMetadata.Builder(shortcutId).build(); + new Notification.BubbleMetadata.Builder(VALID_CONVO_SHORTCUT_ID).build(); Notification.Builder nb = getMessageStyleNotifBuilder(false /* addDefaultMetadata */, null /* groupKey */, false /* isSummary */); - nb.setShortcutId(shortcutId); + nb.setShortcutId(VALID_CONVO_SHORTCUT_ID); nb.setBubbleMetadata(metadata); StatusBarNotification sbn = new StatusBarNotification(PKG, PKG, 1, "tag", mUid, 0, nb.build(), new UserHandle(mUid), null, 0); NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); - // Pretend the shortcut exists - List<ShortcutInfo> shortcutInfos = new ArrayList<>(); - ShortcutInfo info = mock(ShortcutInfo.class); - when(info.getPackage()).thenReturn(PKG); - when(info.getId()).thenReturn(shortcutId); - when(info.getUserId()).thenReturn(USER_SYSTEM); - when(info.isLongLived()).thenReturn(true); - when(info.isEnabled()).thenReturn(true); - shortcutInfos.add(info); - when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcutInfos); - when(mShortcutServiceInternal.isSharingShortcut(anyInt(), anyString(), anyString(), - anyString(), anyInt(), any())).thenReturn(true); + // Test: Send the bubble notification mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(), @@ -6149,7 +6153,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { // Make sure the shortcut is cached. verify(mShortcutServiceInternal).cacheShortcuts( - anyInt(), any(), eq(PKG), eq(Collections.singletonList(shortcutId)), + anyInt(), any(), eq(PKG), eq(Collections.singletonList(VALID_CONVO_SHORTCUT_ID)), eq(USER_SYSTEM)); // Test: Remove the shortcut @@ -6613,6 +6617,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { convo2.setNotificationChannel(channel2); convos.add(convo2); when(mPreferencesHelper.getConversations(anyString(), anyInt())).thenReturn(convos); + when(mLauncherApps.getShortcuts(any(), any())).thenReturn(null); List<ConversationChannelWrapper> conversations = mBinderService.getConversationsForPackage(PKG_P, mUid).getList(); @@ -6640,6 +6645,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationRecord nr = generateMessageBubbleNotifRecord(mTestNotificationChannel, "testRecordMessages_invalidMsg"); + when(mLauncherApps.getShortcuts(any(), any())).thenReturn(null); mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(), nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId()); waitForIdle(); @@ -6660,17 +6666,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { "testRecordMessages_validMsg", mUid, 0, nb.build(), new UserHandle(mUid), null, 0); NotificationRecord nr = new NotificationRecord(mContext, sbn, mTestNotificationChannel); - // Pretend the shortcut exists - List<ShortcutInfo> shortcutInfos = new ArrayList<>(); - ShortcutInfo info = mock(ShortcutInfo.class); - when(info.getPackage()).thenReturn(PKG); - when(info.getId()).thenReturn("id"); - when(info.getUserId()).thenReturn(USER_SYSTEM); - when(info.isLongLived()).thenReturn(true); - when(info.isEnabled()).thenReturn(true); - shortcutInfos.add(info); - when(mLauncherApps.getShortcuts(any(), any())).thenReturn(shortcutInfos); - mBinderService.enqueueNotificationWithTag(PKG, PKG, nr.getSbn().getTag(), nr.getSbn().getId(), nr.getSbn().getNotification(), nr.getSbn().getUserId()); waitForIdle(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java index 9f593ce42741..b03596a35c32 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java @@ -39,7 +39,6 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import android.annotation.Nullable; import android.app.ActivityManager; import android.app.IActivityManager; import android.app.Notification; @@ -90,7 +89,7 @@ public class NotificationRecordTest extends UiServiceTestCase { @Mock private PackageManager mPm; @Mock private ContentResolver mContentResolver; - private final String pkg = PKG_N_MR1; + private final String mPkg = PKG_O; private final int uid = 9583; private final int id1 = 1; private final String tag1 = "tag1"; @@ -198,10 +197,14 @@ public class NotificationRecordTest extends UiServiceTestCase { } Notification n = builder.build(); - return new StatusBarNotification(pkg, pkg, id1, tag1, uid, uid, n, mUser, null, uid); + return new StatusBarNotification(mPkg, mPkg, id1, tag1, uid, uid, n, mUser, null, uid); } private StatusBarNotification getMessagingStyleNotification() { + return getMessagingStyleNotification(mPkg); + } + + private StatusBarNotification getMessagingStyleNotification(String pkg) { final Builder builder = new Builder(mMockContext) .setContentTitle("foo") .setSmallIcon(android.R.drawable.sym_def_app_icon); @@ -658,7 +661,7 @@ public class NotificationRecordTest extends UiServiceTestCase { Bundle signals = new Bundle(); signals.putInt(Adjustment.KEY_USER_SENTIMENT, USER_SENTIMENT_NEGATIVE); - record.addAdjustment(new Adjustment(pkg, record.getKey(), signals, null, sbn.getUserId())); + record.addAdjustment(new Adjustment(mPkg, record.getKey(), signals, null, sbn.getUserId())); record.applyAdjustments(); @@ -687,7 +690,7 @@ public class NotificationRecordTest extends UiServiceTestCase { Bundle signals = new Bundle(); signals.putInt(Adjustment.KEY_USER_SENTIMENT, USER_SENTIMENT_NEGATIVE); - record.addAdjustment(new Adjustment(pkg, record.getKey(), signals, null, sbn.getUserId())); + record.addAdjustment(new Adjustment(mPkg, record.getKey(), signals, null, sbn.getUserId())); record.applyAdjustments(); assertEquals(USER_SENTIMENT_POSITIVE, record.getUserSentiment()); @@ -705,7 +708,7 @@ public class NotificationRecordTest extends UiServiceTestCase { Bundle signals = new Bundle(); signals.putInt(Adjustment.KEY_USER_SENTIMENT, USER_SENTIMENT_NEGATIVE); - record.addAdjustment(new Adjustment(pkg, record.getKey(), signals, null, sbn.getUserId())); + record.addAdjustment(new Adjustment(mPkg, record.getKey(), signals, null, sbn.getUserId())); record.applyAdjustments(); @@ -1134,6 +1137,15 @@ public class NotificationRecordTest extends UiServiceTestCase { } @Test + public void testIsConversation_noShortcut_targetsR() { + StatusBarNotification sbn = getMessagingStyleNotification(PKG_R); + NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel); + record.setShortcutInfo(null); + + assertFalse(record.isConversation()); + } + + @Test public void testIsConversation_channelDemoted() { StatusBarNotification sbn = getMessagingStyleNotification(); channel.setDemoted(true); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 2ea58a028a0a..fdc5c7bf0ce1 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -836,7 +836,7 @@ public class WindowOrganizerTests extends WindowTestsBase { spyOn(record); doReturn(true).when(record).checkEnterPictureInPictureState(any(), anyBoolean()); - record.getRootTask().setHasBeenVisible(true); + record.getTask().setHasBeenVisible(true); return record; } diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java index b7779fd40990..4e75b7354baa 100644 --- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java @@ -1166,6 +1166,7 @@ class UserUsageStatsService { byte[] getBackupPayload(String key){ checkAndGetTimeLocked(); + persistActiveStats(); return mDatabase.getBackupPayload(key); } diff --git a/telephony/common/com/android/internal/telephony/CellBroadcastUtils.java b/telephony/common/com/android/internal/telephony/CellBroadcastUtils.java new file mode 100644 index 000000000000..6c6375586225 --- /dev/null +++ b/telephony/common/com/android/internal/telephony/CellBroadcastUtils.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.telephony; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.provider.Telephony; +import android.text.TextUtils; +import android.util.Log; + +/** + * This class provides utility functions related to CellBroadcast. + */ +public class CellBroadcastUtils { + private static final String TAG = "CellBroadcastUtils"; + private static final boolean VDBG = false; + + /** + * Utility method to query the default CBR's package name. + */ + public static String getDefaultCellBroadcastReceiverPackageName(Context context) { + PackageManager packageManager = context.getPackageManager(); + ResolveInfo resolveInfo = packageManager.resolveActivity( + new Intent(Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION), + PackageManager.MATCH_SYSTEM_ONLY); + String packageName; + + if (resolveInfo == null) { + Log.e(TAG, "getDefaultCellBroadcastReceiverPackageName: no package found"); + return null; + } + + packageName = resolveInfo.activityInfo.applicationInfo.packageName; + + if (VDBG) { + Log.d(TAG, "getDefaultCellBroadcastReceiverPackageName: found package: " + packageName); + } + + if (TextUtils.isEmpty(packageName) || packageManager.checkPermission( + android.Manifest.permission.READ_CELL_BROADCASTS, packageName) + == PackageManager.PERMISSION_DENIED) { + Log.e(TAG, "getDefaultCellBroadcastReceiverPackageName: returning null; " + + "permission check failed for : " + packageName); + return null; + } + + return packageName; + } +} |