Merge "Fix tests that request screen orientation change" into sc-mainline-prod
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 46e36fc..b4a098b 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -14,6 +14,9 @@
},
{
"name": "CtsMediaProviderTranscodeTests[com.google.android.mediaprovider.apex]"
+ },
+ {
+ "name": "CtsPhotoPickerTest[com.google.android.mediaprovider.apex]"
}
],
"presubmit": [
diff --git a/apex/framework/java/android/provider/CloudMediaProvider.java b/apex/framework/java/android/provider/CloudMediaProvider.java
index a3e52c8..bf2e55c 100644
--- a/apex/framework/java/android/provider/CloudMediaProvider.java
+++ b/apex/framework/java/android/provider/CloudMediaProvider.java
@@ -16,7 +16,10 @@
package android.provider;
+import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER;
+import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED;
+import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_EVENT_CALLBACK;
import static android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER;
import static android.provider.CloudMediaProviderContract.METHOD_GET_ACCOUNT_INFO;
import static android.provider.CloudMediaProviderContract.METHOD_GET_MEDIA_INFO;
@@ -28,6 +31,7 @@
import static android.provider.CloudMediaProviderContract.URI_PATH_SURFACE_CONTROLLER;
import android.annotation.DurationMillisLong;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
@@ -44,12 +48,16 @@
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
+import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import java.io.FileNotFoundException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
/**
* Base class for a cloud media provider. A cloud media provider offers read-only access to durable
@@ -105,6 +113,9 @@
private static final int MATCH_MEDIA_INFO = 5;
private static final int MATCH_SURFACE_CONTROLLER = 6;
+ private static final boolean DEFAULT_LOOPING_PLAYBACK_ENABLED = true;
+ private static final boolean DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED = false;
+
private final UriMatcher mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private volatile int mMediaStoreAuthorityAppId;
@@ -306,13 +317,17 @@
* <p>This is meant to be called on the main thread, hence the implementation should not block
* by performing any heavy operation.
*
- * @param extras containing configuration parameters for {@link SurfaceController}
+ * @param config containing configuration parameters for {@link SurfaceController}
* <ul>
* <li> {@link CloudMediaProviderContract#EXTRA_LOOPING_PLAYBACK_ENABLED}
+ * <li> {@link CloudMediaProviderContract#EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED}
* </ul>
+ * @param callback {@link SurfaceEventCallback} to send event updates for {@link Surface} to
+ * picker launched via {@link MediaStore#ACTION_PICK_IMAGES}
*/
@Nullable
- public SurfaceController onCreateSurfaceController(@Nullable Bundle extras) {
+ public SurfaceController onCreateSurfaceController(@NonNull Bundle config,
+ @NonNull SurfaceEventCallback callback) {
return null;
}
@@ -342,19 +357,38 @@
} else if (METHOD_GET_ACCOUNT_INFO.equals(method)) {
return onGetAccountInfo(extras);
} else if (METHOD_CREATE_SURFACE_CONTROLLER.equals(method)) {
- SurfaceController controller = onCreateSurfaceController(extras);
- Bundle bundle = new Bundle();
- if (controller == null) {
- return bundle;
- }
- bundle.putBinder(EXTRA_SURFACE_CONTROLLER,
- new SurfaceControllerWrapper(controller).asBinder());
- return bundle;
+ return onCreateSurfaceController(extras);
} else {
throw new UnsupportedOperationException("Method not supported " + method);
}
}
+ private Bundle onCreateSurfaceController(@NonNull Bundle extras) {
+ Objects.requireNonNull(extras);
+
+ final IBinder binder = extras.getBinder(EXTRA_SURFACE_EVENT_CALLBACK);
+ if (binder == null) {
+ throw new IllegalArgumentException("Missing surface event callback");
+ }
+
+ final SurfaceEventCallback callback =
+ new SurfaceEventCallback(ICloudSurfaceEventCallback.Stub.asInterface(binder));
+ final Bundle config = new Bundle();
+ config.putBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, DEFAULT_LOOPING_PLAYBACK_ENABLED);
+ config.putBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED,
+ DEFAULT_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED);
+ final SurfaceController controller = onCreateSurfaceController(config, callback);
+ if (controller == null) {
+ Log.d(TAG, "onCreateSurfaceController returned null");
+ return Bundle.EMPTY;
+ }
+
+ Bundle result = new Bundle();
+ result.putBinder(EXTRA_SURFACE_CONTROLLER,
+ new SurfaceControllerWrapper(controller).asBinder());
+ return result;
+ }
+
/**
* Implementation is provided by the parent class. Cannot be overridden.
*
@@ -608,8 +642,19 @@
* @param surfaceId id which uniquely identifies the {@link Surface} for rendering
* @param timestampMillis the timestamp in milliseconds from the start to seek to
*/
- public abstract void onMediaSeekTo(int surfaceId,
- @DurationMillisLong long timestampMillis);
+ public abstract void onMediaSeekTo(int surfaceId, @DurationMillisLong long timestampMillis);
+
+ /**
+ * Changes the configuration parameters for the SurfaceController.
+ *
+ * @param config the updated config to change to. This can include config changes for the
+ * following:
+ * <ul>
+ * <li> {@link CloudMediaProviderContract#EXTRA_LOOPING_PLAYBACK_ENABLED}
+ * <li> {@link CloudMediaProviderContract#EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED}
+ * </ul>
+ */
+ public abstract void onConfigChange(@NonNull Bundle config);
/**
* Indicates destruction of this SurfaceController object.
@@ -620,7 +665,93 @@
public abstract void onDestroy();
}
- /** @hide */
+ /**
+ * This class is used by {@link CloudMediaProvider} to send {@link Surface} event updates to
+ * picker launched via {@link MediaStore#ACTION_PICK_IMAGES}.
+ *
+ * @see MediaStore#ACTION_PICK_IMAGES
+ */
+ public static final class SurfaceEventCallback {
+
+ /** {@hide} */
+ @IntDef(flag = true, prefix = { "PLAYBACK_EVENT_" }, value = {
+ PLAYBACK_EVENT_BUFFERING,
+ PLAYBACK_EVENT_READY,
+ PLAYBACK_EVENT_STARTED,
+ PLAYBACK_EVENT_PAUSED,
+ PLAYBACK_EVENT_COMPLETED,
+ PLAYBACK_EVENT_ERROR_RETRIABLE_FAILURE,
+ PLAYBACK_EVENT_ERROR_PERMANENT_FAILURE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PlaybackEvent {}
+
+ /**
+ * Constant to notify that the playback is buffering
+ */
+ public static final int PLAYBACK_EVENT_BUFFERING = 1;
+
+ /**
+ * Constant to notify that the playback is ready to be played
+ */
+ public static final int PLAYBACK_EVENT_READY = 2;
+
+ /**
+ * Constant to notify that the playback has started
+ */
+ public static final int PLAYBACK_EVENT_STARTED = 3;
+
+ /**
+ * Constant to notify that the playback is paused.
+ */
+ public static final int PLAYBACK_EVENT_PAUSED = 4;
+
+ /**
+ * Constant to notify that the playback event has completed
+ */
+ public static final int PLAYBACK_EVENT_COMPLETED = 5;
+
+ /**
+ * Constant to notify that the playback has failed with a retriable error.
+ */
+ public static final int PLAYBACK_EVENT_ERROR_RETRIABLE_FAILURE = 6;
+
+ /**
+ * Constant to notify that the playback has failed with a permanent error.
+ */
+ public static final int PLAYBACK_EVENT_ERROR_PERMANENT_FAILURE = 7;
+
+ private final ICloudSurfaceEventCallback mCallback;
+
+ SurfaceEventCallback (ICloudSurfaceEventCallback callback) {
+ mCallback = callback;
+ }
+
+ /**
+ * This is called to notify playback event update for a {@link Surface}
+ * on the picker launched via {@link MediaStore#ACTION_PICK_IMAGES}.
+ *
+ * @param surfaceId id which uniquely identifies a {@link Surface}
+ * @param playbackEventType playback event type to notify picker about
+ * @param playbackEventInfo {@link Bundle} which may contain extra information about the
+ * playback event. There is no particular event info that
+ * we are currently expecting. This may change if we want to
+ * support more features for Video Preview like progress/seek
+ * bar or show video playback error messages to the user.
+ */
+ public void onPlaybackEvent(int surfaceId, @PlaybackEvent int playbackEventType,
+ @Nullable Bundle playbackEventInfo) {
+ try {
+ mCallback.onPlaybackEvent(surfaceId, playbackEventType, playbackEventInfo);
+ } catch (Exception e) {
+ Log.d(TAG, "Failed to notify playback event (" + playbackEventType + ") for "
+ + "surfaceId: " + surfaceId + " ; playbackEventInfo: " + playbackEventInfo,
+ e);
+ }
+ }
+ }
+
+ /** {@hide} */
private static class SurfaceControllerWrapper extends ICloudMediaSurfaceController.Stub {
final private SurfaceController mSurfaceController;
@@ -681,6 +812,12 @@
}
@Override
+ public void onConfigChange(@NonNull Bundle config) {
+ Log.i(TAG, "Config changed. Updated config params: " + config);
+ mSurfaceController.onConfigChange(config);
+ }
+
+ @Override
public void onDestroy() {
Log.i(TAG, "Controller destroyed");
mSurfaceController.onDestroy();
diff --git a/apex/framework/java/android/provider/CloudMediaProviderContract.java b/apex/framework/java/android/provider/CloudMediaProviderContract.java
index 95d6845..3e0c3c9 100644
--- a/apex/framework/java/android/provider/CloudMediaProviderContract.java
+++ b/apex/framework/java/android/provider/CloudMediaProviderContract.java
@@ -535,14 +535,39 @@
* <p>
* In case this is not present, the default value should be false.
*
- * @see CloudMediaProvider#onCreateSurfaceController(Bundle)
+ * @see CloudMediaProvider#onCreateSurfaceController
+ * @see CloudMediaProvider.SurfaceController#onConfigChange
* <p>
* Type: BOOLEAN
+ * By default, the value is true
*/
public static final String EXTRA_LOOPING_PLAYBACK_ENABLED =
"android.provider.extra.LOOPING_PLAYBACK_ENABLED";
/**
+ * Indicates whether to mute audio during preview of media items.
+ *
+ * @see CloudMediaProvider#onCreateSurfaceController
+ * @see CloudMediaProvider.SurfaceController#onConfigChange
+ * <p>
+ * Type: BOOLEAN
+ * By default, the value is false
+ */
+ public static final String EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED =
+ "android.provider.extra.SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED";
+
+ /**
+ * Gets surface event callback from picker launched via
+ * {@link MediaStore#ACTION_PICK_IMAGES}).
+ *
+ * @see MediaStore#ACTION_PICK_IMAGES
+ *
+ * {@hide}
+ */
+ public static final String EXTRA_SURFACE_EVENT_CALLBACK =
+ "android.provider.extra.SURFACE_EVENT_CALLBACK";
+
+ /**
* URI path for {@link CloudMediaProvider#onQueryMedia}
*
* {@hide}
diff --git a/apex/framework/java/android/provider/ICloudMediaSurfaceController.aidl b/apex/framework/java/android/provider/ICloudMediaSurfaceController.aidl
index 410d42a..a9f48de 100644
--- a/apex/framework/java/android/provider/ICloudMediaSurfaceController.aidl
+++ b/apex/framework/java/android/provider/ICloudMediaSurfaceController.aidl
@@ -34,5 +34,7 @@
void onMediaPause(int surfaceId);
void onMediaSeekTo(int surfaceId, long timestampMillis);
+ void onConfigChange(in Bundle config);
+
void onDestroy();
}
diff --git a/apex/framework/java/android/provider/ICloudSurfaceEventCallback.aidl b/apex/framework/java/android/provider/ICloudSurfaceEventCallback.aidl
new file mode 100644
index 0000000..369da77
--- /dev/null
+++ b/apex/framework/java/android/provider/ICloudSurfaceEventCallback.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.provider;
+
+import android.os.Bundle;
+
+/**
+ * Interface through which Photo Picker receives playback events from the CloudMediaProviders
+ * @hide
+ */
+interface ICloudSurfaceEventCallback {
+ void onPlaybackEvent(int surfaceId, int eventType, in Bundle eventInfo);
+}
diff --git a/res/drawable/ic_volume_off.xml b/res/drawable/ic_volume_off.xml
new file mode 100644
index 0000000..59ebf31
--- /dev/null
+++ b/res/drawable/ic_volume_off.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M20.5,22.6l-3.025,-3.025q-0.625,0.4 -1.325,0.688 -0.7,0.287 -1.45,0.462v-2.05q0.35,-0.125 0.688,-0.25 0.337,-0.125 0.637,-0.3L12.7,14.8L12.7,20l-5,-5h-4L3.7,9h3.2L2.1,4.2l1.4,-1.4 18.4,18.4zM20.3,16.8l-1.45,-1.45q0.425,-0.775 0.637,-1.625 0.213,-0.85 0.213,-1.75 0,-2.35 -1.375,-4.2t-3.625,-2.5v-2.05q3.1,0.7 5.05,3.138Q21.7,8.8 21.7,11.975q0,1.325 -0.363,2.55 -0.362,1.225 -1.037,2.275zM9.8,11.9zM16.95,13.45L14.7,11.2L14.7,7.95q1.175,0.55 1.838,1.65 0.662,1.1 0.662,2.4 0,0.375 -0.063,0.738 -0.062,0.362 -0.187,0.712zM12.7,9.2l-2.6,-2.6L12.7,4zM10.7,15.15L10.7,12.8L8.9,11L5.7,11v2h2.85z"/>
+</vector>
diff --git a/res/drawable/ic_volume_up.xml b/res/drawable/ic_volume_up.xml
new file mode 100644
index 0000000..9758bb9
--- /dev/null
+++ b/res/drawable/ic_volume_up.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M14,20.725v-2.05q2.25,-0.65 3.625,-2.5t1.375,-4.2q0,-2.35 -1.375,-4.2T14,5.275v-2.05q3.1,0.7 5.05,3.138Q21,8.8 21,11.975q0,3.175 -1.95,5.612 -1.95,2.438 -5.05,3.138zM3,15L3,9h4l5,-5v16l-5,-5zM14,16L14,7.95q1.125,0.525 1.813,1.625 0.687,1.1 0.687,2.425 0,1.325 -0.688,2.4Q15.126,15.475 14,16zM10,8.85L7.85,11L5,11v2h2.85L10,15.15zM7.5,12z"/>
+</vector>
diff --git a/res/drawable/preview_mute.xml b/res/drawable/preview_mute.xml
new file mode 100644
index 0000000..3b62d1e
--- /dev/null
+++ b/res/drawable/preview_mute.xml
@@ -0,0 +1,4 @@
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_selected="true" android:drawable="@drawable/ic_volume_off"/>
+ <item android:drawable="@drawable/ic_volume_up"/>
+</selector>
\ No newline at end of file
diff --git a/res/layout/preview_video_controls.xml b/res/layout/preview_video_controls.xml
index 3e3029a..a145435 100644
--- a/res/layout/preview_video_controls.xml
+++ b/res/layout/preview_video_controls.xml
@@ -13,17 +13,28 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@id/exo_center_controls"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:background="@android:color/transparent"
- android:gravity="center"
- android:padding="@dimen/exo_styled_controls_padding"
- android:clipToPadding="false">
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@id/exo_center_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:background="@android:color/transparent"
+ android:gravity="center"
+ android:padding="@dimen/exo_styled_controls_padding"
+ android:clipToPadding="false">
- <ImageButton android:id="@id/exo_play_pause"
- style="@style/ExoStyledControls.Button.Center.PlayPause"/>
+ <ImageButton android:id="@id/exo_play_pause"
+ style="@style/ExoStyledControls.Button.Center.PlayPause"/>
+ </FrameLayout>
-</FrameLayout>
\ No newline at end of file
+ <ImageButton android:id="@+id/preview_mute"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:src="@drawable/preview_mute"
+ android:layout_gravity="end|bottom"
+ android:layout_marginTop="@dimen/exo_styled_bottom_bar_margin_top"
+ android:layout_marginEnd="@dimen/preview_mute_marginEnd"
+ android:layout_marginBottom="@dimen/preview_mute_marginBottom"
+ android:background="@android:color/transparent" />
+</merge>
\ No newline at end of file
diff --git a/res/values-v31/dimens.xml b/res/values-v31/dimens.xml
index 31ec80c..760fef9 100644
--- a/res/values-v31/dimens.xml
+++ b/res/values-v31/dimens.xml
@@ -20,4 +20,6 @@
<dimen name="picker_bottom_bar_size">72dp</dimen>
+ <dimen name="preview_mute_marginBottom">88dp</dimen>
+
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index ce75c5e..dd79997 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -75,6 +75,8 @@
<dimen name="preview_viewpager_margin">20dp</dimen>
<dimen name="preview_gif_icon_size">32dp</dimen>
<dimen name="preview_add_or_select_width">328dp</dimen>
+ <dimen name="preview_mute_marginEnd">16dp</dimen>
+ <dimen name="preview_mute_marginBottom">68dp</dimen>
<!-- PhotoPicker Preview text -->
<dimen name="preview_special_format_text_size">12sp</dimen>
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
index ffbd44c..4386388 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerActivity.java
@@ -21,7 +21,10 @@
import android.annotation.IntDef;
import android.app.Activity;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Color;
@@ -32,6 +35,7 @@
import android.os.Binder;
import android.os.Bundle;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.util.Log;
import android.view.LayoutInflater;
@@ -47,12 +51,15 @@
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import com.android.modules.utils.build.SdkLevel;
import com.android.providers.media.R;
import com.android.providers.media.photopicker.data.Selection;
+import com.android.providers.media.photopicker.data.UserIdManager;
import com.android.providers.media.photopicker.data.model.Category;
+import com.android.providers.media.photopicker.data.model.UserId;
import com.android.providers.media.photopicker.ui.AlbumsTabFragment;
import com.android.providers.media.photopicker.ui.PhotosTabFragment;
import com.android.providers.media.photopicker.ui.PreviewFragment;
@@ -62,9 +69,11 @@
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback;
import com.google.android.material.chip.Chip;
+import com.google.common.collect.Lists;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.List;
/**
* Photo Picker allows users to choose one or more photos and/or videos to share with an app. The
@@ -97,6 +106,7 @@
private View mDragBar;
private View mPrivacyText;
private Toolbar mToolbar;
+ private CrossProfileListeners mCrossProfileListeners;
@TabChipType
private int mSelectedTabChipType;
@@ -157,6 +167,15 @@
// Save the fragment container layout so that we can adjust the padding based on preview or
// non-preview mode.
mFragmentContainerView = findViewById(R.id.fragment_container);
+
+ mCrossProfileListeners = new CrossProfileListeners();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ // This is required to unregister any broadcast receivers.
+ mCrossProfileListeners.onDestroy();
}
/**
@@ -269,14 +288,20 @@
}
restoreBottomSheetState();
} else {
- // This is the first launch, set the default behavior. Hide the title, show the chips
- // and show the PhotosTabFragment
- updateCommonLayouts(MODE_PHOTOS_TAB, /* title */ "");
- onTabChipClick(mPhotosTabChip);
- saveBottomSheetState();
+ setupInitialLaunchState();
}
}
+ /**
+ * Sets up states for the initial launch. This includes updating common layouts, selecting
+ * Photos tab chip and saving the current bottom sheet state for later.
+ */
+ private void setupInitialLaunchState() {
+ updateCommonLayouts(MODE_PHOTOS_TAB, /* title */ "");
+ onTabChipClick(mPhotosTabChip);
+ saveBottomSheetState();
+ }
+
private static Chip generateTabChip(LayoutInflater inflater, ViewGroup parent, String title) {
final Chip chip = (Chip) inflater.inflate(R.layout.picker_chip_tab_header, parent, false);
chip.setText(title);
@@ -570,4 +595,114 @@
final boolean shouldShowPrivacyMessage = mode.isPhotosTabOrAlbumsTab;
mPrivacyText.setVisibility(shouldShowPrivacyMessage ? View.VISIBLE : View.GONE);
}
+
+ private class CrossProfileListeners {
+
+ private final List<String> MANAGED_PROFILE_FILTER_ACTIONS = Lists.newArrayList(
+ Intent.ACTION_MANAGED_PROFILE_ADDED, // add profile button switch
+ Intent.ACTION_MANAGED_PROFILE_REMOVED, // remove profile button switch
+ Intent.ACTION_MANAGED_PROFILE_UNLOCKED, // activate profile button switch
+ Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE // disable profile button switch
+ );
+
+ private final UserIdManager mUserIdManager;
+
+ public CrossProfileListeners() {
+ mUserIdManager = mPickerViewModel.getUserIdManager();
+
+ registerBroadcastReceivers();
+ }
+
+ public void onDestroy() {
+ unregisterReceiver(mReceiver);
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+
+ final UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER);
+ final UserId userId = UserId.of(userHandle);
+
+ // We only need to refresh the layout when the received profile user is the
+ // managed user corresponding to the current profile or a new work profile is added
+ // for the current user.
+ if (!userId.equals(mUserIdManager.getManagedUserId()) &&
+ !action.equals(Intent.ACTION_MANAGED_PROFILE_ADDED)) {
+ return;
+ }
+
+ switch (action) {
+ case Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE:
+ handleWorkProfileOff();
+ break;
+ case Intent.ACTION_MANAGED_PROFILE_REMOVED:
+ handleWorkProfileRemoved();
+ break;
+ case Intent.ACTION_MANAGED_PROFILE_UNLOCKED:
+ handleWorkProfileOn();
+ break;
+ case Intent.ACTION_MANAGED_PROFILE_ADDED:
+ handleWorkProfileAdded();
+ break;
+ default:
+ // do nothing
+ }
+ }
+ };
+
+ private void registerBroadcastReceivers() {
+ final IntentFilter managedProfileFilter = new IntentFilter();
+ for (String managedProfileAction : MANAGED_PROFILE_FILTER_ACTIONS) {
+ managedProfileFilter.addAction(managedProfileAction);
+ }
+ registerReceiver(mReceiver, managedProfileFilter);
+ }
+
+ private void handleWorkProfileOff() {
+ if (mUserIdManager.isManagedUserSelected()) {
+ switchToPersonalProfileInitialLaunchState();
+ }
+ mUserIdManager.updateWorkProfileOffValue();
+ }
+
+ private void handleWorkProfileRemoved() {
+ if (mUserIdManager.isManagedUserSelected()) {
+ switchToPersonalProfileInitialLaunchState();
+ }
+ mUserIdManager.resetUserIds();
+ }
+
+ private void handleWorkProfileAdded() {
+ mUserIdManager.resetUserIds();
+ }
+
+ private void handleWorkProfileOn() {
+ // Update UI for switch to profile button
+ // When the managed profile becomes available, the provider may not be available
+ // immediately, we need to check if it is ready before we reload the content.
+ mUserIdManager.waitForMediaProviderToBeAvailable();
+ }
+
+ private void switchToPersonalProfileInitialLaunchState() {
+ // We reset the state of the PhotoPicker as we do not want to make any
+ // assumptions on the state of the PhotoPicker when it was in Work Profile mode.
+ resetToPersonalProfile();
+
+ final FragmentManager fragmentManager = getSupportFragmentManager();
+ // This is important so that doing a back does not take back to work profile fragment
+ // state.
+ fragmentManager.popBackStack();
+ PhotosTabFragment.show(fragmentManager, Category.getDefaultCategory());
+ }
+
+ /**
+ * Reset to Photo Picker initial launch state (Photos grid tab) in personal profile mode.
+ */
+ private void resetToPersonalProfile() {
+ mPickerViewModel.resetToPersonalProfile();
+ setupInitialLaunchState();
+ }
+ }
}
diff --git a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
index 576ce99..820161b 100644
--- a/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
+++ b/src/com/android/providers/media/photopicker/PhotoPickerProvider.java
@@ -17,6 +17,7 @@
package com.android.providers.media.photopicker;
import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
+import static android.provider.CloudMediaProvider.SurfaceEventCallback.PLAYBACK_EVENT_READY;
import static android.provider.CloudMediaProviderContract.MediaInfo;
import android.annotation.DurationMillisLong;
@@ -161,11 +162,12 @@
@Override
@Nullable
- public SurfaceController onCreateSurfaceController(@Nullable Bundle extras) {
+ public SurfaceController onCreateSurfaceController(@Nullable Bundle config,
+ SurfaceEventCallback callback) {
if (RemotePreviewHandler.isRemotePreviewEnabled()) {
- boolean enableLoop = extras != null && extras.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED,
+ boolean enableLoop = config != null && config.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED,
false);
- return new SurfaceControllerImpl(getContext(), enableLoop);
+ return new SurfaceControllerImpl(getContext(), enableLoop, callback);
}
return null;
}
@@ -205,12 +207,14 @@
BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS).build();
private final Context mContext;
+ private final SurfaceEventCallback mCallback;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final boolean mEnableLoop;
private ExoPlayer mPlayer;
private int mCurrentSurfaceId = -1;
- SurfaceControllerImpl(Context context, boolean enableLoop) {
+ SurfaceControllerImpl(Context context, boolean enableLoop, SurfaceEventCallback callback) {
+ mCallback = callback;
mContext = context;
mEnableLoop = enableLoop;
Log.d(TAG, "Surface controller created.");
@@ -249,10 +253,8 @@
mPlayer.setVideoSurface(surface);
mCurrentSurfaceId = surfaceId;
mPlayer.prepare();
- // TODO(b/215175249): Don't directly start playback here, just send an event
- // instead. It should be up to the photo picker to decide when to start
- // playback.
- mPlayer.setPlayWhenReady(true);
+
+ mCallback.onPlaybackEvent(surfaceId, PLAYBACK_EVENT_READY, null);
Log.d(TAG, "Surface prepared: " + surfaceId + ". Surface: " + surface
+ ". MediaId: " + mediaId);
@@ -313,6 +315,13 @@
}
@Override
+ public void onConfigChange(@NonNull Bundle config) {
+ // TODO(b/195009562): Implement mute/unmute audio and loop enabled/disabled
+ // for video preview
+ Log.d(TAG, "Config changed. Updated config params: " + config);
+ }
+
+ @Override
public void onDestroy() {
Log.d(TAG, "Surface controller destroyed.");
}
diff --git a/src/com/android/providers/media/photopicker/data/ItemsProvider.java b/src/com/android/providers/media/photopicker/data/ItemsProvider.java
index 38f594a..9c41579 100644
--- a/src/com/android/providers/media/photopicker/data/ItemsProvider.java
+++ b/src/com/android/providers/media/photopicker/data/ItemsProvider.java
@@ -220,6 +220,11 @@
final Uri contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL);
try (ContentProviderClient client = userId.getContentResolver(mContext)
.acquireUnstableContentProviderClient(MediaStore.AUTHORITY)) {
+ if (client == null) {
+ Log.e(TAG, "Unable to acquire unstable content provider for "
+ + MediaStore.AUTHORITY);
+ return null;
+ }
Bundle extras = new Bundle();
extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION, selection);
extras.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs);
@@ -254,6 +259,11 @@
final Bundle extras = new Bundle();
try (ContentProviderClient client = userId.getContentResolver(mContext)
.acquireUnstableContentProviderClient(MediaStore.AUTHORITY)) {
+ if (client == null) {
+ Log.e(TAG, "Unable to acquire unstable content provider for "
+ + MediaStore.AUTHORITY);
+ return null;
+ }
extras.putInt(MediaStore.QUERY_ARG_LIMIT, limit);
extras.putString(MediaStore.QUERY_ARG_MIME_TYPE, mimeType);
if (category != null) {
@@ -280,6 +290,11 @@
final Bundle extras = new Bundle();
try (ContentProviderClient client = userId.getContentResolver(mContext)
.acquireUnstableContentProviderClient(MediaStore.AUTHORITY)) {
+ if (client == null) {
+ Log.e(TAG, "Unable to acquire unstable content provider for "
+ + MediaStore.AUTHORITY);
+ return null;
+ }
extras.putString(MediaStore.QUERY_ARG_MIME_TYPE, mimeType);
final Uri uri = PickerUriResolver.PICKER_INTERNAL_URI.buildUpon()
diff --git a/src/com/android/providers/media/photopicker/data/MuteStatus.java b/src/com/android/providers/media/photopicker/data/MuteStatus.java
new file mode 100644
index 0000000..b6e76e3
--- /dev/null
+++ b/src/com/android/providers/media/photopicker/data/MuteStatus.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.photopicker.data;
+
+/**
+ * Tracks the status of volume mute request from the user.
+ */
+public final class MuteStatus {
+ /**
+ * Always start video preview with volume off
+ */
+ private boolean isVolumeMuted = true;
+
+ public MuteStatus() {};
+
+ /**
+ * Sets the volume to mute/unmute
+ * @param isVolumeMuted - {@code true} if the volume state should be set to mute.
+ * {@code false} otherwise.
+ */
+ public void setVolumeMuted(boolean isVolumeMuted) {
+ this.isVolumeMuted = isVolumeMuted;
+ }
+
+ /**
+ * @return {@code isVolumeMuted}
+ */
+ public boolean isVolumeMuted() {
+ return isVolumeMuted;
+ }
+}
diff --git a/src/com/android/providers/media/photopicker/data/Selection.java b/src/com/android/providers/media/photopicker/data/Selection.java
index 7d7883c..79692ba 100644
--- a/src/com/android/providers/media/photopicker/data/Selection.java
+++ b/src/com/android/providers/media/photopicker/data/Selection.java
@@ -95,6 +95,15 @@
}
/**
+ * Clear all selected items
+ */
+ public void clearSelectedItems() {
+ mSelectedItems.clear();
+ mSelectedItemSize.postValue(mSelectedItems.size());
+ updateSelectionAllowed();
+ }
+
+ /**
* @return {@code true} if give {@code item} is present in selected items
* {@link #mSelectedItems}, {@code false} otherwise
*/
diff --git a/src/com/android/providers/media/photopicker/data/UserIdManager.java b/src/com/android/providers/media/photopicker/data/UserIdManager.java
index 1f265de..07d8c5c 100644
--- a/src/com/android/providers/media/photopicker/data/UserIdManager.java
+++ b/src/com/android/providers/media/photopicker/data/UserIdManager.java
@@ -18,18 +18,19 @@
import static androidx.core.util.Preconditions.checkNotNull;
+import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.WorkerThread;
-import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
-import android.content.IntentFilter;
import android.content.pm.PackageManager;
+import android.os.Handler;
+import android.os.Looper;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
-import com.android.internal.annotations.GuardedBy;
+import androidx.lifecycle.MutableLiveData;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.providers.media.photopicker.data.model.UserId;
import com.android.providers.media.photopicker.util.CrossProfileUtils;
@@ -122,10 +123,32 @@
void setIntentAndCheckRestrictions(Intent intent);
/**
- * Updates cross profile restrictions values
+ * Waits for Media Provider of the work profile to be available.
*/
- @WorkerThread
- void updateCrossProfileValues();
+ void waitForMediaProviderToBeAvailable();
+
+ /**
+ * Checks if work profile is switched off and updates the data.
+ */
+ void updateWorkProfileOffValue();
+
+ /**
+ * Resets the user ids. This is usually called as a result of receiving broadcast that
+ * managed profile has been added or removed.
+ */
+ void resetUserIds();
+
+ /**
+ * @return {@link MutableLiveData} to check if cross profile interaction allowed or not
+ */
+ @NonNull
+ MutableLiveData<Boolean> getCrossProfileAllowed();
+
+ /**
+ * @return {@link MutableLiveData} to check if there are multiple user profiles or not
+ */
+ @NonNull
+ MutableLiveData<Boolean> getIsMultiUserProfiles();
/**
* Creates an implementation of {@link UserIdManager}.
@@ -135,41 +158,34 @@
}
/**
- * Implementation of {@link UserIdManager}.
+ * Implementation of {@link UserIdManager}. The class assumes that all its public methods are
+ * called from main thread only.
*/
final class RuntimeUserIdManager implements UserIdManager {
private static final String TAG = "UserIdManager";
+ // These values are copied from DocumentsUI
+ private static final int PROVIDER_AVAILABILITY_MAX_RETRIES = 10;
+ private static final long PROVIDER_AVAILABILITY_CHECK_DELAY = 4000;
+
private final Context mContext;
private final UserId mCurrentUser;
+ private final Handler mHandler;
- @GuardedBy("mLock")
- private final Object mLock = new Object();
- @GuardedBy("mLock")
+ private Runnable mIsProviderAvailableRunnable;
+
private UserId mPersonalUser = null;
- @GuardedBy("mLock")
private UserId mManagedUser = null;
- @GuardedBy("mLock")
private UserId mCurrentUserProfile = null;
- private Intent mIntent = null;
// Set default values to negative case, only set as false if checks pass.
private boolean mIsBlockedByAdmin = true;
private boolean mIsWorkProfileOff = true;
- private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- synchronized (mLock) {
- mPersonalUser = null;
- mManagedUser = null;
- setUserIds();
- }
- }
- };
+ private final MutableLiveData<Boolean> mIsMultiUserProfiles = new MutableLiveData<>();
+ private final MutableLiveData<Boolean> mIsCrossProfileAllowed = new MutableLiveData<>();
private RuntimeUserIdManager(Context context) {
this(context, UserId.CURRENT_USER);
@@ -180,94 +196,104 @@
mContext = context.getApplicationContext();
mCurrentUser = checkNotNull(currentUser);
mCurrentUserProfile = mCurrentUser;
+ mHandler = new Handler(Looper.getMainLooper());
setUserIds();
+ }
- IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_MANAGED_PROFILE_ADDED);
- filter.addAction(Intent.ACTION_MANAGED_PROFILE_REMOVED);
- mContext.registerReceiver(mIntentReceiver, filter);
+ @Override
+ public MutableLiveData<Boolean> getCrossProfileAllowed() {
+ return mIsCrossProfileAllowed;
+ }
+
+ @Override
+ public MutableLiveData<Boolean> getIsMultiUserProfiles() {
+ return mIsMultiUserProfiles;
+ }
+
+ @Override
+ public void resetUserIds() {
+ assertMainThread();
+ setUserIds();
}
@Override
public boolean isMultiUserProfiles() {
- synchronized (mLock) {
- return mPersonalUser != null;
- }
+ assertMainThread();
+ return mPersonalUser != null;
}
@Override
public UserId getPersonalUserId() {
- synchronized (mLock) {
- return mPersonalUser;
- }
+ assertMainThread();
+ return mPersonalUser;
}
@Override
public UserId getManagedUserId() {
- synchronized (mLock) {
- return mManagedUser;
- }
+ assertMainThread();
+ return mManagedUser;
}
@Override
public UserId getCurrentUserProfileId() {
- synchronized (mLock) {
- return mCurrentUserProfile;
- }
+ assertMainThread();
+ return mCurrentUserProfile;
}
@Override
public void setManagedAsCurrentUserProfile() {
+ assertMainThread();
setCurrentUserProfileId(getManagedUserId());
}
@Override
public void setPersonalAsCurrentUserProfile() {
+ assertMainThread();
setCurrentUserProfileId(getPersonalUserId());
}
@Override
public void setIntentAndCheckRestrictions(Intent intent) {
- mIntent = intent;
- updateCrossProfileValues();
+ assertMainThread();
+ if (isMultiUserProfiles()) {
+ updateCrossProfileValues(intent);
+ }
}
public boolean isCurrentUserSelected() {
- synchronized (mLock) {
- return mCurrentUserProfile.equals(UserId.CURRENT_USER);
- }
+ assertMainThread();
+ return mCurrentUserProfile.equals(UserId.CURRENT_USER);
}
public boolean isManagedUserSelected() {
- synchronized (mLock) {
- return mCurrentUserProfile.equals(getManagedUserId());
- }
+ assertMainThread();
+ return mCurrentUserProfile.equals(getManagedUserId());
}
@Override
public boolean isPersonalUserId() {
+ assertMainThread();
return mCurrentUser.equals(getPersonalUserId());
}
@Override
public boolean isManagedUserId() {
+ assertMainThread();
return mCurrentUser.equals(getManagedUserId());
}
- private void setUserIds() {
- synchronized (mLock) {
- setUserIdsInternalLocked();
- }
- }
-
private void setCurrentUserProfileId(UserId userId) {
- synchronized (mLock) {
- mCurrentUserProfile = userId;
- }
+ mCurrentUserProfile = userId;
}
- @GuardedBy("mLock")
- private void setUserIdsInternalLocked() {
+ private void setUserIds() {
+ setUserIdsInternal();
+ mIsMultiUserProfiles.postValue(isMultiUserProfiles());
+ }
+
+ private void setUserIdsInternal() {
+ mPersonalUser = null;
+ mManagedUser = null;
UserManager userManager = mContext.getSystemService(UserManager.class);
if (userManager == null) {
Log.e(TAG, "Cannot obtain user manager");
@@ -308,49 +334,126 @@
@Override
public boolean isCrossProfileAllowed() {
+ assertMainThread();
return (!isWorkProfileOff() && !isBlockedByAdmin());
}
@Override
public boolean isWorkProfileOff() {
+ assertMainThread();
return mIsWorkProfileOff;
}
@Override
public boolean isBlockedByAdmin() {
+ assertMainThread();
return mIsBlockedByAdmin;
}
@Override
- @WorkerThread
- public void updateCrossProfileValues() {
- setCrossProfileValues();
+ public void updateWorkProfileOffValue() {
+ assertMainThread();
+ mIsWorkProfileOff = isWorkProfileOffInternal(getManagedUserId());
+ mIsCrossProfileAllowed.postValue(isCrossProfileAllowed());
}
- @WorkerThread
- private void setCrossProfileValues() {
- final PackageManager packageManager = mContext.getPackageManager();
+ @Override
+ public void waitForMediaProviderToBeAvailable() {
+ assertMainThread();
+ final UserId managedUserProfileId = getManagedUserId();
+ if (CrossProfileUtils.isMediaProviderAvailable(managedUserProfileId, mContext)) {
+ mIsWorkProfileOff = false;
+ mIsCrossProfileAllowed.postValue(isCrossProfileAllowed());
+ stopWaitingForProviderToBeAvailable();
+ return;
+ }
+ waitForProviderToBeAvailable(managedUserProfileId, /* numOfTries */ 1);
+ }
+
+ private void updateCrossProfileValues(Intent intent) {
+ setCrossProfileValues(intent);
+ mIsCrossProfileAllowed.postValue(isCrossProfileAllowed());
+ }
+
+ private void setCrossProfileValues(Intent intent) {
// 1. Check if PICK_IMAGES intent is allowed by admin to show cross user content
- if (mIntent == null) {
+ setBlockedByAdminValue(intent);
+
+ // 2. Check if work profile is off
+ updateWorkProfileOffValue();
+
+ // 3. For first initial setup, wait for MediaProvider to be on.
+ // (This is not blocking)
+ if (mIsWorkProfileOff) {
+ waitForMediaProviderToBeAvailable();
+ }
+ }
+
+ private void setBlockedByAdminValue(Intent intent) {
+ if (intent == null) {
Log.e(TAG, "No intent specified to check if cross profile forwarding is"
+ " allowed.");
return;
}
- if (!CrossProfileUtils.isIntentAllowedCrossProfileAccess(mIntent, packageManager)) {
+ final PackageManager packageManager = mContext.getPackageManager();
+ if (!CrossProfileUtils.isIntentAllowedCrossProfileAccess(intent, packageManager)) {
mIsBlockedByAdmin = true;
return;
}
mIsBlockedByAdmin = false;
+ }
- // 2. Check if work profile is off
- if (!isManagedUserSelected()) {
- final UserId managedUserProfileId = getManagedUserId();
- if (!CrossProfileUtils.isMediaProviderAvailable(managedUserProfileId, mContext)) {
- mIsWorkProfileOff = true;
+ private boolean isWorkProfileOffInternal(UserId managedUserProfileId) {
+ return CrossProfileUtils.isQuietModeEnabled(managedUserProfileId, mContext) ||
+ !CrossProfileUtils.isMediaProviderAvailable(managedUserProfileId, mContext);
+ }
+
+ private void waitForProviderToBeAvailable(UserId userId, int numOfTries) {
+ // The runnable should make sure to post update on the live data if it is changed.
+ mIsProviderAvailableRunnable = () -> {
+ // We stop the recursive check when
+ // 1. the provider is available
+ // 2. the profile is in quiet mode, i.e. provider will not be available
+ // 3. after maximum retries
+ if (CrossProfileUtils.isMediaProviderAvailable(userId, mContext)) {
+ mIsWorkProfileOff = false;
+ mIsCrossProfileAllowed.postValue(isCrossProfileAllowed());
return;
}
+
+ if (CrossProfileUtils.isQuietModeEnabled(userId, mContext)) {
+ return;
+ }
+
+ if (numOfTries <= PROVIDER_AVAILABILITY_MAX_RETRIES) {
+ Log.d(TAG, "MediaProvider is not available. Retry after " +
+ PROVIDER_AVAILABILITY_CHECK_DELAY);
+ waitForProviderToBeAvailable(userId, numOfTries + 1);
+ return;
+ }
+
+ Log.w(TAG, "Failed waiting for MediaProvider for user:" + userId +
+ " to be available");
+ };
+
+ mHandler.postDelayed(mIsProviderAvailableRunnable, PROVIDER_AVAILABILITY_CHECK_DELAY);
+ }
+
+ private void stopWaitingForProviderToBeAvailable() {
+ if (mIsProviderAvailableRunnable == null) {
+ return;
}
- mIsWorkProfileOff = false;
+ mHandler.removeCallbacks(mIsProviderAvailableRunnable);
+ mIsProviderAvailableRunnable = null;
+ }
+
+ private void assertMainThread() {
+ if (Looper.getMainLooper().isCurrentThread()) return;
+
+ throw new IllegalStateException("UserIdManager methods are expected to be called from "
+ + "main thread. " + (Looper.myLooper() == null ? "" : "Current thread "
+ + Looper.myLooper().getThread() + ", Main thread "
+ + Looper.getMainLooper().getThread()));
}
}
}
diff --git a/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java b/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java
index f2c6d79..ca3ac9b 100644
--- a/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java
+++ b/src/com/android/providers/media/photopicker/ui/ExoPlayerWrapper.java
@@ -17,11 +17,15 @@
package com.android.providers.media.photopicker.ui;
import android.content.Context;
+import android.media.AudioManager;
import android.net.Uri;
+import android.util.Log;
import android.view.View;
+import android.widget.ImageButton;
import android.widget.ImageView;
import com.android.providers.media.R;
+import com.android.providers.media.photopicker.data.MuteStatus;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
@@ -43,6 +47,7 @@
* that all its public methods are called from main thread only.
*/
class ExoPlayerWrapper {
+ private static final String TAG = "ExoPlayerWrapper";
// The minimum duration of media that the player will attempt to ensure is buffered at all
// times.
private static final int MIN_BUFFER_MS = 1000;
@@ -62,12 +67,14 @@
private static final long PLAYER_CONTROL_ON_PLAY_TIMEOUT_MS = 1000;
private final Context mContext;
+ private final MuteStatus mMuteStatus;
private ExoPlayer mExoPlayer;
private boolean mIsPlayerReleased = true;
private boolean mShouldShowControlsForNext = true;
- public ExoPlayerWrapper(Context context) {
+ public ExoPlayerWrapper(Context context, MuteStatus muteStatus) {
mContext = context;
+ mMuteStatus = muteStatus;
}
/**
@@ -172,6 +179,34 @@
PLAYER_CONTROL_ON_PLAY_TIMEOUT_MS);
}
});
+
+ // Step3: Set-up mute button
+ final ImageButton muteButton = styledPlayerView.findViewById(R.id.preview_mute);
+ final boolean isVolumeMuted = mMuteStatus.isVolumeMuted();
+ // Set the status of the muteButton according to previous status of the mute button
+ muteButton.setSelected(isVolumeMuted);
+ if (isVolumeMuted) {
+ // If the previous volume was muted, set the volume status to mute.
+ mExoPlayer.setVolume(0f);
+ }
+
+ // Add click listeners for mute button
+ muteButton.setOnClickListener(v -> {
+ if (mMuteStatus.isVolumeMuted()) {
+ AudioManager audioManager = mContext.getSystemService(AudioManager.class);
+ if (audioManager == null) {
+ Log.e(TAG, "Couldn't find AudioManager while trying to set volume,"
+ + " unable to set volume");
+ return;
+ }
+ mExoPlayer.setVolume(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC));
+ mMuteStatus.setVolumeMuted(false);
+ } else {
+ mExoPlayer.setVolume(0f);
+ mMuteStatus.setVolumeMuted(true);
+ }
+ muteButton.setSelected(mMuteStatus.isVolumeMuted());
+ });
}
private void releaseIfNecessary() {
diff --git a/src/com/android/providers/media/photopicker/ui/PlaybackHandler.java b/src/com/android/providers/media/photopicker/ui/PlaybackHandler.java
index 5322ddd..26f4bd4 100644
--- a/src/com/android/providers/media/photopicker/ui/PlaybackHandler.java
+++ b/src/com/android/providers/media/photopicker/ui/PlaybackHandler.java
@@ -24,6 +24,7 @@
import android.widget.ImageView;
import com.android.providers.media.R;
+import com.android.providers.media.photopicker.data.MuteStatus;
import com.android.providers.media.photopicker.data.model.Item;
import com.google.android.exoplayer2.ui.StyledPlayerView;
@@ -41,8 +42,8 @@
private final ExoPlayerWrapper mExoPlayerWrapper;
private final ImageLoader mImageLoader;
- PlaybackHandler(Context context, ImageLoader imageLoader) {
- mExoPlayerWrapper = new ExoPlayerWrapper(context);
+ PlaybackHandler(Context context, ImageLoader imageLoader, MuteStatus muteStatus) {
+ mExoPlayerWrapper = new ExoPlayerWrapper(context, muteStatus);
mImageLoader = imageLoader;
}
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java b/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
index 413ada7..d563e85 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewAdapter.java
@@ -23,6 +23,7 @@
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.providers.media.photopicker.data.MuteStatus;
import com.android.providers.media.photopicker.data.model.Item;
import com.android.providers.media.photopicker.ui.remotepreview.RemotePreviewHandler;
@@ -44,10 +45,10 @@
private final boolean mIsRemotePreviewEnabled =
RemotePreviewHandler.isRemotePreviewEnabled();
- PreviewAdapter(Context context) {
+ PreviewAdapter(Context context, MuteStatus muteStatus) {
mImageLoader = new ImageLoader(context);
mRemotePreviewHandler = new RemotePreviewHandler(context);
- mPlaybackHandler = new PlaybackHandler(context, mImageLoader);
+ mPlaybackHandler = new PlaybackHandler(context, mImageLoader, muteStatus);
}
@NonNull
diff --git a/src/com/android/providers/media/photopicker/ui/PreviewFragment.java b/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
index a5ae60e..d9e074e 100644
--- a/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/PreviewFragment.java
@@ -39,6 +39,7 @@
import com.android.providers.media.R;
import com.android.providers.media.photopicker.PhotoPickerActivity;
+import com.android.providers.media.photopicker.data.MuteStatus;
import com.android.providers.media.photopicker.data.Selection;
import com.android.providers.media.photopicker.data.model.Item;
import com.android.providers.media.photopicker.util.LayoutModeUtils;
@@ -71,6 +72,7 @@
private ViewPager2Wrapper mViewPager2Wrapper;
private boolean mShouldShowGifBadge;
private boolean mShouldShowMotionPhotoBadge;
+ private MuteStatus mMuteStatus;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -98,8 +100,10 @@
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
- mSelection = new ViewModelProvider(requireActivity())
- .get(PickerViewModel.class).getSelection();
+ mSelection = new ViewModelProvider(requireActivity()).get(PickerViewModel.class)
+ .getSelection();
+ mMuteStatus = new ViewModelProvider(requireActivity()).get(PickerViewModel.class)
+ .getMuteStatus();
return inflater.inflate(R.layout.fragment_preview, parent, /* attachToRoot */ false);
}
@@ -123,7 +127,7 @@
throw new IllegalStateException("Expected to find ViewPager2 in " + view
+ ", but found null");
}
- mViewPager2Wrapper = new ViewPager2Wrapper(viewPager, selectedItemsList);
+ mViewPager2Wrapper = new ViewPager2Wrapper(viewPager, selectedItemsList, mMuteStatus);
setUpPreviewLayout(view, getArguments());
setupScrimLayerAndBottomBar(view);
diff --git a/src/com/android/providers/media/photopicker/ui/TabFragment.java b/src/com/android/providers/media/photopicker/ui/TabFragment.java
index c5626ab..ddc36c5 100644
--- a/src/com/android/providers/media/photopicker/ui/TabFragment.java
+++ b/src/com/android/providers/media/photopicker/ui/TabFragment.java
@@ -30,6 +30,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView;
@@ -38,7 +40,6 @@
import com.android.providers.media.photopicker.data.Selection;
import com.android.providers.media.photopicker.data.UserIdManager;
import com.android.providers.media.photopicker.viewmodel.PickerViewModel;
-import com.android.providers.media.util.ForegroundThread;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
@@ -49,6 +50,7 @@
* The base abstract Tab fragment
*/
public abstract class TabFragment extends Fragment {
+
protected PickerViewModel mPickerViewModel;
protected Selection mSelection;
protected ImageLoader mImageLoader;
@@ -141,6 +143,25 @@
});
}
+ // Initial setup
+ setUpProfileButtonWithListeners(mUserIdManager.isMultiUserProfiles());
+
+ // Observe for cross profile access changes.
+ final LiveData<Boolean> crossProfileAllowed = mUserIdManager.getCrossProfileAllowed();
+ if (crossProfileAllowed != null) {
+ crossProfileAllowed.observe(this, isCrossProfileAllowed -> {
+ setUpProfileButton();
+ });
+ }
+
+ // Observe for multi-user changes.
+ final LiveData<Boolean> isMultiUserProfiles = mUserIdManager.getIsMultiUserProfiles();
+ if (isMultiUserProfiles != null) {
+ isMultiUserProfiles.observe(this, this::setUpProfileButtonWithListeners);
+ }
+ }
+
+ private void setUpListenersForProfileButton() {
mProfileButton.setOnClickListener(v -> onClickProfileButton());
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
@@ -156,12 +177,6 @@
}
@Override
- public void onResume() {
- super.onResume();
- updateProfileButtonAsync();
- }
-
- @Override
public void onDestroy() {
super.onDestroy();
if (mRecyclerView != null) {
@@ -169,12 +184,13 @@
}
}
- private void updateProfileButtonAsync() {
- ForegroundThread.getExecutor().execute(() -> {
- mUserIdManager.updateCrossProfileValues();
-
- getActivity().runOnUiThread(() -> setUpProfileButton());
- });
+ private void setUpProfileButtonWithListeners(boolean isMultiUserProfile) {
+ if (isMultiUserProfile) {
+ setUpListenersForProfileButton();
+ } else {
+ mRecyclerView.clearOnScrollListeners();
+ }
+ setUpProfileButton();
}
private void setUpProfileButton() {
diff --git a/src/com/android/providers/media/photopicker/ui/ViewPager2Wrapper.java b/src/com/android/providers/media/photopicker/ui/ViewPager2Wrapper.java
index 2c5ed72..563f777 100644
--- a/src/com/android/providers/media/photopicker/ui/ViewPager2Wrapper.java
+++ b/src/com/android/providers/media/photopicker/ui/ViewPager2Wrapper.java
@@ -24,6 +24,7 @@
import androidx.viewpager2.widget.ViewPager2;
import com.android.providers.media.R;
+import com.android.providers.media.photopicker.data.MuteStatus;
import com.android.providers.media.photopicker.data.model.Item;
import java.util.ArrayList;
@@ -41,12 +42,12 @@
private final PreviewAdapter mAdapter;
private final List<ViewPager2.OnPageChangeCallback> mOnPageChangeCallbacks = new ArrayList<>();
- ViewPager2Wrapper(ViewPager2 viewPager, List<Item> selectedItems) {
+ ViewPager2Wrapper(ViewPager2 viewPager, List<Item> selectedItems, MuteStatus muteStatus) {
mViewPager = viewPager;
final Context context = mViewPager.getContext();
- mAdapter = new PreviewAdapter(context);
+ mAdapter = new PreviewAdapter(context, muteStatus);
mAdapter.updateItemList(selectedItems);
mViewPager.setAdapter(mAdapter);
diff --git a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
index b03d9d4..1a4010f 100644
--- a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
+++ b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewHandler.java
@@ -18,7 +18,9 @@
import static android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED;
import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER;
+import static android.provider.CloudMediaProviderContract.EXTRA_SURFACE_EVENT_CALLBACK;
import static android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER;
+import static android.provider.CloudMediaProvider.SurfaceEventCallback.PLAYBACK_EVENT_READY;
import static com.android.providers.media.PickerUriResolver.createSurfaceControllerUri;
@@ -29,6 +31,8 @@
import android.os.RemoteException;
import android.os.SystemProperties;
import android.provider.ICloudMediaSurfaceController;
+
+import android.provider.ICloudSurfaceEventCallback;
import android.util.ArrayMap;
import android.util.Log;
import android.view.Surface;
@@ -56,6 +60,7 @@
private final Map<String, SurfaceControllerProxy> mControllers =
new ArrayMap<>();
private final SurfaceHolder.Callback mSurfaceHolderCallback = new PreviewSurfaceCallback();
+ private final SurfaceEventCallbackWrapper mSurfaceEventCallbackWrapper;
private Item mCurrentPreviewItem;
private boolean mIsInBackground = false;
@@ -67,6 +72,7 @@
public RemotePreviewHandler(Context context) {
mContext = context;
+ mSurfaceEventCallbackWrapper = new SurfaceEventCallbackWrapper();
}
/**
@@ -164,6 +170,15 @@
return null;
}
+ private RemotePreviewSession getSessionForSurfaceId(int surfaceId) {
+ for (RemotePreviewSession session : mSessionMap.values()) {
+ if (session.getSurfaceId() == surfaceId) {
+ return session;
+ }
+ }
+ return null;
+ }
+
@Nullable
private SurfaceControllerProxy getSurfaceController(String authority) {
if (mControllers.containsKey(authority)) {
@@ -197,6 +212,7 @@
Log.i(TAG, "Creating new SurfaceController for authority: " + authority);
Bundle extras = new Bundle();
extras.putBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, true);
+ extras.putBinder(EXTRA_SURFACE_EVENT_CALLBACK, mSurfaceEventCallbackWrapper);
final Bundle surfaceControllerBundle = mContext.getContentResolver().call(
createSurfaceControllerUri(authority),
METHOD_CREATE_SURFACE_CONTROLLER, /* arg */ null, extras);
@@ -206,6 +222,32 @@
: null;
}
+ /**
+ * Wrapper class for {@link ICloudSurfaceEventCallback} interface implementation.
+ */
+ private final class SurfaceEventCallbackWrapper extends ICloudSurfaceEventCallback.Stub {
+
+ @Override
+ public void onPlaybackEvent(int surfaceId, int eventType, Bundle eventInfo) {
+ final RemotePreviewSession session = getSessionForSurfaceId(surfaceId);
+
+ if (session == null) {
+ Log.w(TAG, "No RemotePreviewSession found.");
+ return;
+ }
+ switch (eventType) {
+ case PLAYBACK_EVENT_READY:
+ session.playMedia();
+ return;
+ default:
+ Log.d(TAG, "RemotePreviewHandler onPlaybackEvent for surfaceId: " +
+ surfaceId + " ; media id: " + session.getMediaId() +
+ " ; eventType: " + eventType + " ; eventInfo: " + eventInfo);
+
+ }
+ }
+ }
+
private final class PreviewSurfaceCallback implements SurfaceHolder.Callback {
@Override
diff --git a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
index 3f8fc4d..35b421b 100644
--- a/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
+++ b/src/com/android/providers/media/photopicker/ui/remotepreview/RemotePreviewSession.java
@@ -44,6 +44,10 @@
this.mSurfaceController = surfaceController;
}
+ int getSurfaceId() {
+ return mSurfaceId;
+ }
+
@NonNull
String getMediaId() {
return mMediaId;
diff --git a/src/com/android/providers/media/photopicker/util/CrossProfileUtils.java b/src/com/android/providers/media/photopicker/util/CrossProfileUtils.java
index a9b345c..301b623 100644
--- a/src/com/android/providers/media/photopicker/util/CrossProfileUtils.java
+++ b/src/com/android/providers/media/photopicker/util/CrossProfileUtils.java
@@ -21,6 +21,7 @@
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.os.UserManager;
import android.provider.MediaStore;
import android.util.Log;
@@ -73,4 +74,16 @@
}
return false;
}
+
+ /**
+ * Whether the given profile is in quiet mode or not.
+ * Notes: Quiet mode is only supported for managed profiles.
+ *
+ * @param userId The user id of the profile to be queried.
+ * @return true if the profile is in quiet mode, false otherwise.
+ */
+ public static boolean isQuietModeEnabled(UserId userId, Context context) {
+ final UserManager userManager = context.getSystemService(UserManager.class);
+ return userManager.isQuietModeEnabled(userId.getUserHandle());
+ }
}
diff --git a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
index cdc1b72..b5f708a 100644
--- a/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
+++ b/src/com/android/providers/media/photopicker/viewmodel/PickerViewModel.java
@@ -34,6 +34,7 @@
import androidx.lifecycle.MutableLiveData;
import com.android.providers.media.photopicker.data.ItemsProvider;
+import com.android.providers.media.photopicker.data.MuteStatus;
import com.android.providers.media.photopicker.data.Selection;
import com.android.providers.media.photopicker.data.UserIdManager;
import com.android.providers.media.photopicker.data.model.Category;
@@ -53,8 +54,9 @@
public static final String TAG = "PhotoPicker";
private static final int RECENT_MINIMUM_COUNT = 12;
-
+
private final Selection mSelection;
+ private final MuteStatus mMuteStatus;
// TODO(b/193857982): We keep these four data sets now, we may need to find a way to reduce the
// data set to reduce memories.
@@ -77,6 +79,7 @@
mItemsProvider = new ItemsProvider(context);
mSelection = new Selection();
mUserIdManager = UserIdManager.create(context);
+ mMuteStatus = new MuteStatus();
}
@VisibleForTesting
@@ -103,6 +106,27 @@
return mSelection;
}
+
+ /**
+ * @return {@code mMuteStatus} that tracks the volume mute status of the video preview
+ */
+ public MuteStatus getMuteStatus() {
+ return mMuteStatus;
+ }
+
+ /**
+ * Reset to personal profile mode.
+ */
+ public void resetToPersonalProfile() {
+ // 1. Clear Selected items
+ mSelection.clearSelectedItems();
+ // 2. Change profile to personal user
+ mUserIdManager.setPersonalAsCurrentUserProfile();
+ // 3. Update Item and Category lists
+ updateItems();
+ updateCategories();
+ }
+
/**
* @return the list of Items with all photos and videos {@link #mItemList} on the device.
*/
@@ -113,9 +137,8 @@
return mItemList;
}
- private List<Item> loadItems(@Nullable @CategoryType String category) {
+ private List<Item> loadItems(@Nullable @CategoryType String category, UserId userId) {
final List<Item> items = new ArrayList<>();
- final UserId userId = mUserIdManager.getCurrentUserProfileId();
try (Cursor cursor = mItemsProvider.getItems(category, /* offset */ 0,
/* limit */ -1, mMimeTypeFilter, userId)) {
@@ -168,8 +191,9 @@
}
private void loadItemsAsync() {
+ final UserId userId = mUserIdManager.getCurrentUserProfileId();
ForegroundThread.getExecutor().execute(() -> {
- mItemList.postValue(loadItems(/* category= */ null));
+ mItemList.postValue(loadItems(/* category= */ null, userId));
});
}
@@ -196,8 +220,9 @@
}
private void loadCategoryItemsAsync(@NonNull @CategoryType String category) {
+ final UserId userId = mUserIdManager.getCurrentUserProfileId();
ForegroundThread.getExecutor().execute(() -> {
- mCategoryItemList.postValue(loadItems(category));
+ mCategoryItemList.postValue(loadItems(category, userId));
});
}
@@ -221,9 +246,8 @@
return mCategoryList;
}
- private List<Category> loadCategories() {
+ private List<Category> loadCategories(UserId userId) {
final List<Category> categoryList = new ArrayList<>();
- final UserId userId = mUserIdManager.getCurrentUserProfileId();
try (final Cursor cursor = mItemsProvider.getCategories(mMimeTypeFilter, userId)) {
if (cursor == null || cursor.getCount() == 0) {
Log.d(TAG, "Didn't receive any categories, either cursor is null or"
@@ -243,8 +267,9 @@
}
private void loadCategoriesAsync() {
+ final UserId userId = mUserIdManager.getCurrentUserProfileId();
ForegroundThread.getExecutor().execute(() -> {
- mCategoryList.postValue(loadCategories());
+ mCategoryList.postValue(loadCategories(userId));
});
}
diff --git a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
index 0ccbafe..49dfaf6 100644
--- a/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
+++ b/tests/src/com/android/providers/media/photopicker/ItemsProviderTest.java
@@ -391,12 +391,12 @@
public void testGetItems_sortOrder() throws Exception {
try {
final long timeNow = System.nanoTime() / 1000;
+ final Uri imageFileDateNowPlus1Uri = prepareFileAndGetUri(
+ new File(getDownloadsDir(), "latest_" + IMAGE_FILE_NAME), timeNow + 1000);
final Uri imageFileDateNowUri
- = createFileAndGet(getDcimDir(), IMAGE_FILE_NAME, timeNow);
+ = prepareFileAndGetUri(new File(getDcimDir(), IMAGE_FILE_NAME), timeNow);
final Uri videoFileDateNowUri
- = createFileAndGet(getCameraDir(), VIDEO_FILE_NAME, timeNow);
- final Uri imageFileDateNowPlus1Uri = createFileAndGet(getDownloadsDir(),
- "latest_" + IMAGE_FILE_NAME, timeNow + 1000);
+ = prepareFileAndGetUri(new File(getCameraDir(), VIDEO_FILE_NAME), timeNow);
// This is the list of uris based on the expected sort order of items returned by
// ItemsProvider#getItems
@@ -747,19 +747,16 @@
return assertCreateNewFile(getDownloadsDir(), IMAGE_FILE_NAME);
}
- private File assertCreateNewFile(File dir, String fileName) throws Exception {
- if (!dir.exists()) {
- dir.mkdirs();
- }
- assertThat(dir.exists()).isTrue();
-
- final File file = new File(dir, fileName);
- createAndPrepareFile(file);
+ private File assertCreateNewFile(File parentDir, String fileName) throws Exception {
+ final File file = new File(parentDir, fileName);
+ prepareFileAndGetUri(file, /* lastModifiedTime */ -1);
return file;
}
- private Uri createAndPrepareFile(File file) throws IOException {
+ private Uri prepareFileAndGetUri(File file, long lastModifiedTime) throws IOException {
+ ensureParentExists(file.getParentFile());
+
assertThat(file.createNewFile()).isTrue();
// Write 1 byte because 0byte files are not valid in the picker db
@@ -767,12 +764,26 @@
fos.write(1);
}
+ if (lastModifiedTime != -1) {
+ file.setLastModified(lastModifiedTime);
+ }
+
final Uri uri = MediaStore.scanFile(mIsolatedResolver, file);
+ assertWithMessage("Uri obtained by scanning file " + file)
+ .that(uri).isNotNull();
+ // Wait for picker db sync
MediaStore.waitForIdle(mIsolatedResolver);
return uri;
}
+ private void ensureParentExists(File parent) {
+ if (!parent.exists()) {
+ parent.mkdirs();
+ }
+ assertThat(parent.exists()).isTrue();
+ }
+
private File getDownloadsDir() {
return new File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS);
}
@@ -825,14 +836,4 @@
}
}
-
- private Uri createFileAndGet(File parent, String fileName, long lastModifiedTime)
- throws IOException {
- final File file = new File(parent, fileName);
- final Uri uri = createAndPrepareFile(file);
-
- assertWithMessage("Uri obtained by scanning file " + file)
- .that(uri).isNotNull();
- return uri;
- }
}
diff --git a/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java b/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java
index 4a7eaa1..ec68646 100644
--- a/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/SelectionTest.java
@@ -57,7 +57,9 @@
final Context context = InstrumentationRegistry.getTargetContext();
when(mApplication.getApplicationContext()).thenReturn(context);
- mSelection = new PickerViewModel(mApplication).getSelection();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ mSelection = new PickerViewModel(mApplication).getSelection();
+ });
}
@Test
@@ -90,6 +92,23 @@
}
@Test
+ public void testClearSelectedItem() {
+ final String id = "1";
+ final Item item = generateFakeImageItem(id);
+ final String id2 = "2";
+ final Item item2 = generateFakeImageItem(id2);
+
+ assertThat(mSelection.getSelectedItemCount().getValue()).isEqualTo(0);
+
+ mSelection.addSelectedItem(item);
+ mSelection.addSelectedItem(item2);
+ assertThat(mSelection.getSelectedItemCount().getValue()).isEqualTo(2);
+
+ mSelection.clearSelectedItems();
+ assertThat(mSelection.getSelectedItemCount().getValue()).isEqualTo(0);
+ }
+
+ @Test
public void testSetSelectedItem() {
final String id1 = "1";
final Item item1 = generateFakeImageItem(id1);
diff --git a/tests/src/com/android/providers/media/photopicker/data/UserIdManagerTest.java b/tests/src/com/android/providers/media/photopicker/data/UserIdManagerTest.java
index 93d3cc8..83f8fab 100644
--- a/tests/src/com/android/providers/media/photopicker/data/UserIdManagerTest.java
+++ b/tests/src/com/android/providers/media/photopicker/data/UserIdManagerTest.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -26,6 +27,8 @@
import android.os.UserHandle;
import android.os.UserManager;
+import androidx.test.platform.app.InstrumentationRegistry;
+
import com.android.providers.media.photopicker.data.model.UserId;
import org.junit.Before;
@@ -60,19 +63,34 @@
when(mockContext.getSystemService(UserManager.class)).thenReturn(mockUserManager);
}
+ @Test
+ public void testUserIdManagerThrowsErrorIfCalledFromNonMainThread() {
+ UserId currentUser = UserId.of(personalUser);
+ initializeUserIdManager(currentUser, Arrays.asList(personalUser));
+
+ assertThrows(IllegalStateException.class, () -> userIdManager.isMultiUserProfiles());
+ assertThrows(IllegalStateException.class, () -> userIdManager.getCurrentUserProfileId());
+ assertThrows(IllegalStateException.class, () -> userIdManager.isPersonalUserId());
+ assertThrows(IllegalStateException.class, () -> userIdManager.isManagedUserId());
+ assertThrows(IllegalStateException.class, () -> userIdManager.getPersonalUserId());
+ assertThrows(IllegalStateException.class, () -> userIdManager.getManagedUserId());
+ }
+
// common cases for User Profiles
@Test
public void testUserIds_personaUser_currentUserIsPersonalUser() {
// Returns the current user if there is only 1 user.
UserId currentUser = UserId.of(personalUser);
initializeUserIdManager(currentUser, Arrays.asList(personalUser));
- assertThat(userIdManager.isMultiUserProfiles()).isFalse();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ assertThat(userIdManager.isMultiUserProfiles()).isFalse();
- assertThat(userIdManager.isPersonalUserId()).isFalse();
- assertThat(userIdManager.isManagedUserId()).isFalse();
+ assertThat(userIdManager.isPersonalUserId()).isFalse();
+ assertThat(userIdManager.isManagedUserId()).isFalse();
- assertThat(userIdManager.getPersonalUserId()).isNull();
- assertThat(userIdManager.getManagedUserId()).isNull();
+ assertThat(userIdManager.getPersonalUserId()).isNull();
+ assertThat(userIdManager.getManagedUserId()).isNull();
+ });
}
@Test
@@ -80,13 +98,15 @@
// Returns both if there are personal and managed users.
UserId currentUser = UserId.of(personalUser);
initializeUserIdManager(currentUser, Arrays.asList(personalUser, managedUser1));
- assertThat(userIdManager.isMultiUserProfiles()).isTrue();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ assertThat(userIdManager.isMultiUserProfiles()).isTrue();
- assertThat(userIdManager.isPersonalUserId()).isTrue();
- assertThat(userIdManager.isManagedUserId()).isFalse();
+ assertThat(userIdManager.isPersonalUserId()).isTrue();
+ assertThat(userIdManager.isManagedUserId()).isFalse();
- assertThat(userIdManager.getPersonalUserId()).isEqualTo(currentUser);
- assertThat(userIdManager.getManagedUserId()).isEqualTo(UserId.of(managedUser1));
+ assertThat(userIdManager.getPersonalUserId()).isEqualTo(currentUser);
+ assertThat(userIdManager.getManagedUserId()).isEqualTo(UserId.of(managedUser1));
+ });
}
@Test
@@ -94,13 +114,15 @@
// Returns both if there are system and managed users.
UserId currentUser = UserId.of(managedUser1);
initializeUserIdManager(currentUser, Arrays.asList(personalUser, managedUser1));
- assertThat(userIdManager.isMultiUserProfiles()).isTrue();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ assertThat(userIdManager.isMultiUserProfiles()).isTrue();
- assertThat(userIdManager.isPersonalUserId()).isFalse();
- assertThat(userIdManager.isManagedUserId()).isTrue();
+ assertThat(userIdManager.isPersonalUserId()).isFalse();
+ assertThat(userIdManager.isManagedUserId()).isTrue();
- assertThat(userIdManager.getPersonalUserId()).isEqualTo(UserId.of(personalUser));
- assertThat(userIdManager.getManagedUserId()).isEqualTo(currentUser);
+ assertThat(userIdManager.getPersonalUserId()).isEqualTo(UserId.of(personalUser));
+ assertThat(userIdManager.getManagedUserId()).isEqualTo(currentUser);
+ });
}
// other cases for User Profiles involving different users
@@ -109,28 +131,31 @@
// When there is no managed user, returns the current user.
UserId currentUser = UserId.of(otherUser2);
initializeUserIdManager(currentUser, Arrays.asList(otherUser1, otherUser2));
- assertThat(userIdManager.isMultiUserProfiles()).isFalse();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ assertThat(userIdManager.isMultiUserProfiles()).isFalse();
- assertThat(userIdManager.isPersonalUserId()).isFalse();
- assertThat(userIdManager.isManagedUserId()).isFalse();
+ assertThat(userIdManager.isPersonalUserId()).isFalse();
+ assertThat(userIdManager.isManagedUserId()).isFalse();
- assertThat(userIdManager.getPersonalUserId()).isNull();
- assertThat(userIdManager.getManagedUserId()).isNull();
+ assertThat(userIdManager.getPersonalUserId()).isNull();
+ assertThat(userIdManager.getManagedUserId()).isNull();
+ });
}
@Test
- public void testUserIds_otherUserAndManagedUserAndPersonalUser_currentUserIsOtherUser(
- ) {
+ public void testUserIds_otherUserAndManagedUserAndPersonalUser_currentUserIsOtherUser() {
UserId currentUser = UserId.of(otherUser1);
initializeUserIdManager(currentUser, Arrays.asList(otherUser1, managedUser1,
personalUser));
- assertThat(userIdManager.isMultiUserProfiles()).isFalse();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ assertThat(userIdManager.isMultiUserProfiles()).isFalse();
- assertThat(userIdManager.isPersonalUserId()).isFalse();
- assertThat(userIdManager.isManagedUserId()).isFalse();
+ assertThat(userIdManager.isPersonalUserId()).isFalse();
+ assertThat(userIdManager.isManagedUserId()).isFalse();
- assertThat(userIdManager.getPersonalUserId()).isNull();
- assertThat(userIdManager.getManagedUserId()).isNull();
+ assertThat(userIdManager.getPersonalUserId()).isNull();
+ assertThat(userIdManager.getManagedUserId()).isNull();
+ });
}
@Test
@@ -141,27 +166,33 @@
UserId currentUser = UserId.of(managedUser1);
initializeUserIdManager(currentUser, Arrays.asList(otherUser1, managedUser1,
personalUser));
- assertThat(userIdManager.isMultiUserProfiles()).isTrue();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ assertThat(userIdManager.isMultiUserProfiles()).isTrue();
- assertThat(userIdManager.isPersonalUserId()).isFalse();
- assertThat(userIdManager.isManagedUserId()).isTrue();
+ assertThat(userIdManager.isPersonalUserId()).isFalse();
+ assertThat(userIdManager.isManagedUserId()).isTrue();
- assertThat(userIdManager.getPersonalUserId()).isEqualTo(UserId.of(personalUser));
- assertThat(userIdManager.getManagedUserId()).isEqualTo(currentUser);
+ assertThat(userIdManager.getPersonalUserId()).isEqualTo(UserId.of(personalUser));
+ assertThat(userIdManager.getManagedUserId()).isEqualTo(currentUser);
+ });
}
@Test
public void testUserIds_personalUserAndManagedUser_returnCachedList() {
UserId currentUser = UserId.of(personalUser);
initializeUserIdManager(currentUser, Arrays.asList(personalUser, managedUser1));
- assertThat(userIdManager.getPersonalUserId()).isSameInstanceAs(
- userIdManager.getPersonalUserId());
- assertThat(userIdManager.getManagedUserId()).isSameInstanceAs(
- userIdManager.getManagedUserId());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ assertThat(userIdManager.getPersonalUserId()).isSameInstanceAs(
+ userIdManager.getPersonalUserId());
+ assertThat(userIdManager.getManagedUserId()).isSameInstanceAs(
+ userIdManager.getManagedUserId());
+ });
}
private void initializeUserIdManager(UserId current, List<UserHandle> usersOnDevice) {
when(mockUserManager.getUserProfiles()).thenReturn(usersOnDevice);
- userIdManager = new UserIdManager.RuntimeUserIdManager(mockContext, current);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ userIdManager = new UserIdManager.RuntimeUserIdManager(mockContext, current);
+ });
}
}
diff --git a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
index 9b995f5..fc5c5a5 100644
--- a/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
+++ b/tests/src/com/android/providers/media/photopicker/viewmodel/PickerViewModelTest.java
@@ -20,6 +20,7 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.app.Application;
@@ -35,6 +36,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.providers.media.photopicker.data.ItemsProvider;
+import com.android.providers.media.photopicker.data.UserIdManager;
import com.android.providers.media.photopicker.data.model.Category;
import com.android.providers.media.photopicker.data.model.Item;
import com.android.providers.media.photopicker.data.model.ItemTest;
@@ -73,9 +75,14 @@
final Context context = InstrumentationRegistry.getTargetContext();
when(mApplication.getApplicationContext()).thenReturn(context);
- mPickerViewModel = new PickerViewModel(mApplication);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ mPickerViewModel = new PickerViewModel(mApplication);
+ });
mItemsProvider = new TestItemsProvider(context);
mPickerViewModel.setItemsProvider(mItemsProvider);
+ UserIdManager userIdManager = mock(UserIdManager.class);
+ when(userIdManager.getCurrentUserProfileId()).thenReturn(UserId.CURRENT_USER);
+ mPickerViewModel.setUserIdManager(userIdManager);
}
@Test