blob: 195fca85a314366a5774272259cdc6b3cb07b304 [file] [log] [blame]
Youngsang Chof1647922015-12-17 13:39:39 -08001/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Winsonbf8c2c02016-10-18 18:56:24 -070017package com.android.systemui.pip.tv;
Youngsang Chof1647922015-12-17 13:39:39 -080018
Winson Chung67f5c8b2018-09-24 12:09:19 -070019import static android.app.ActivityTaskManager.INVALID_STACK_ID;
20import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
21import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
Winson Chung67f5c8b2018-09-24 12:09:19 -070022
Winsonc0d70582016-01-29 10:24:39 -080023import android.app.ActivityManager.RunningTaskInfo;
Youngsang Chof1647922015-12-17 13:39:39 -080024import android.app.ActivityManager.StackInfo;
Wale Ogunwale65ebd952018-04-25 15:41:44 -070025import android.app.ActivityTaskManager;
Wale Ogunwale65ebd952018-04-25 15:41:44 -070026import android.app.IActivityTaskManager;
Youngsang Chof1647922015-12-17 13:39:39 -080027import android.content.BroadcastReceiver;
Jaewan Kim62338192016-02-25 10:00:05 -080028import android.content.ComponentName;
Youngsang Chof1647922015-12-17 13:39:39 -080029import android.content.Context;
30import android.content.Intent;
31import android.content.IntentFilter;
Winson Chung2a82fe52017-02-02 14:43:34 -080032import android.content.pm.ParceledListSlice;
Winson Chung0c5a5922017-05-22 17:41:06 -070033import android.content.res.Configuration;
Youngsang Chof1647922015-12-17 13:39:39 -080034import android.content.res.Resources;
35import android.graphics.Rect;
Jaewan Kim62338192016-02-25 10:00:05 -080036import android.media.session.MediaController;
37import android.media.session.MediaSessionManager;
Jaewan Kim8f584b82016-03-22 22:16:59 +090038import android.media.session.PlaybackState;
Wale Ogunwale480dca02016-02-06 13:58:29 -080039import android.os.Debug;
Youngsang Chof1647922015-12-17 13:39:39 -080040import android.os.Handler;
41import android.os.RemoteException;
Kyeongkab.Namd61de152018-10-09 14:55:42 +090042import android.os.UserHandle;
Dongwon Kang353d8d72016-04-13 21:55:22 -070043import android.text.TextUtils;
Youngsang Chof1647922015-12-17 13:39:39 -080044import android.util.Log;
Jaewan Kimd89a6942016-04-08 11:41:45 +090045import android.util.Pair;
Hongwei Wang43a752b2019-09-17 20:20:30 +000046import android.view.DisplayInfo;
Gus Prevasab336792018-11-14 13:52:20 -050047
Winson Chung67f5c8b2018-09-24 12:09:19 -070048import com.android.systemui.Dependency;
Jaewan Kim8f584b82016-03-22 22:16:59 +090049import com.android.systemui.R;
Winson Chung67f5c8b2018-09-24 12:09:19 -070050import com.android.systemui.UiOffloadThread;
Fabian Kozynski5ca7a512019-10-16 19:56:11 +000051import com.android.systemui.broadcast.BroadcastDispatcher;
Winson Chung29a78652017-02-09 18:35:26 -080052import com.android.systemui.pip.BasePipManager;
Hongwei Wang43a752b2019-09-17 20:20:30 +000053import com.android.systemui.pip.PipBoundsHandler;
Winson Chung2cf6ad82017-11-09 17:36:59 -080054import com.android.systemui.shared.system.ActivityManagerWrapper;
Hongwei Wang43a752b2019-09-17 20:20:30 +000055import com.android.systemui.shared.system.PinnedStackListenerForwarder.PinnedStackListener;
Winson Chung67f5c8b2018-09-24 12:09:19 -070056import com.android.systemui.shared.system.TaskStackChangeListener;
57import com.android.systemui.shared.system.WindowManagerWrapper;
Gus Prevasab336792018-11-14 13:52:20 -050058
Youngsang Chof1647922015-12-17 13:39:39 -080059import java.util.ArrayList;
60import java.util.List;
61
Youngsang Chof1647922015-12-17 13:39:39 -080062/**
63 * Manages the picture-in-picture (PIP) UI and states.
64 */
Winson Chung29a78652017-02-09 18:35:26 -080065public class PipManager implements BasePipManager {
Youngsang Chof1647922015-12-17 13:39:39 -080066 private static final String TAG = "PipManager";
Jaewan Kim26c63562017-04-26 15:41:43 +090067 static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
68
Jaewan Kim8af250e2017-02-14 19:12:01 +090069 private static final String SETTINGS_PACKAGE_AND_CLASS_DELIMITER = "/";
Youngsang Chof1647922015-12-17 13:39:39 -080070
71 private static PipManager sPipManager;
Jaewan Kim8af250e2017-02-14 19:12:01 +090072 private static List<Pair<String, String>> sSettingsPackageAndClassNamePairList;
Jaewan Kimd89a6942016-04-08 11:41:45 +090073
74 /**
Jaewan Kima0d4d252016-03-31 13:37:10 +090075 * State when there's no PIP.
76 */
Wale Ogunwale480dca02016-02-06 13:58:29 -080077 public static final int STATE_NO_PIP = 0;
Jaewan Kima0d4d252016-03-31 13:37:10 +090078 /**
Jaewan Kime6309232017-04-14 15:26:20 +090079 * State when PIP is shown. This is used as default PIP state.
Jaewan Kima0d4d252016-03-31 13:37:10 +090080 */
Jaewan Kime6309232017-04-14 15:26:20 +090081 public static final int STATE_PIP = 1;
Jaewan Kima0d4d252016-03-31 13:37:10 +090082 /**
83 * State when PIP menu dialog is shown.
84 */
Wale Ogunwale480dca02016-02-06 13:58:29 -080085 public static final int STATE_PIP_MENU = 2;
Youngsang Chof1647922015-12-17 13:39:39 -080086
Youngsang Choad8ceb02016-01-15 16:59:27 -080087 private static final int TASK_ID_NO_PIP = -1;
88 private static final int INVALID_RESOURCE_TYPE = -1;
89
Wale Ogunwale480dca02016-02-06 13:58:29 -080090 public static final int SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_MENU_ACTIVITY_FINISH = 0x1;
91 public static final int SUSPEND_PIP_RESIZE_REASON_WAITING_FOR_OVERLAY_ACTIVITY_FINISH = 0x2;
Jaewan Kimf40fcdc2016-03-04 17:58:22 +090092
Jaewan Kim8f584b82016-03-22 22:16:59 +090093 /**
94 * PIPed activity is playing a media and it can be paused.
95 */
96 static final int PLAYBACK_STATE_PLAYING = 0;
97 /**
98 * PIPed activity has a paused media and it can be played.
99 */
100 static final int PLAYBACK_STATE_PAUSED = 1;
101 /**
102 * Users are unable to control PIPed activity's media playback.
103 */
104 static final int PLAYBACK_STATE_UNAVAILABLE = 2;
105
Jaewan Kimf40fcdc2016-03-04 17:58:22 +0900106 private static final int CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS = 3000;
107
Wale Ogunwale480dca02016-02-06 13:58:29 -0800108 private int mSuspendPipResizingReason;
109
Youngsang Chof1647922015-12-17 13:39:39 -0800110 private Context mContext;
Hongwei Wang43a752b2019-09-17 20:20:30 +0000111 private PipBoundsHandler mPipBoundsHandler;
Wale Ogunwale65ebd952018-04-25 15:41:44 -0700112 private IActivityTaskManager mActivityTaskManager;
Jaewan Kim62338192016-02-25 10:00:05 -0800113 private MediaSessionManager mMediaSessionManager;
Youngsang Chof1647922015-12-17 13:39:39 -0800114 private int mState = STATE_NO_PIP;
Winson Chung0a657372017-05-12 15:04:54 -0700115 private int mResumeResizePinnedStackRunnableState = STATE_NO_PIP;
Youngsang Chof1647922015-12-17 13:39:39 -0800116 private final Handler mHandler = new Handler();
117 private List<Listener> mListeners = new ArrayList<>();
Jaewan Kima0d4d252016-03-31 13:37:10 +0900118 private List<MediaListener> mMediaListeners = new ArrayList<>();
Jaewan Kimc92a7d12016-02-15 17:33:25 -0800119 private Rect mCurrentPipBounds;
120 private Rect mPipBounds;
Winson Chungdb2ad5d2017-02-07 17:18:01 -0800121 private Rect mDefaultPipBounds = new Rect();
Jaewan Kimd89a6942016-04-08 11:41:45 +0900122 private Rect mSettingsPipBounds;
Jaewan Kimc92a7d12016-02-15 17:33:25 -0800123 private Rect mMenuModePipBounds;
Winson Chung0c5a5922017-05-22 17:41:06 -0700124 private int mLastOrientation = Configuration.ORIENTATION_UNDEFINED;
Youngsang Chof1647922015-12-17 13:39:39 -0800125 private boolean mInitialized;
Youngsang Choad8ceb02016-01-15 16:59:27 -0800126 private int mPipTaskId = TASK_ID_NO_PIP;
Wale Ogunwale89be5762017-10-04 13:27:49 -0700127 private int mPinnedStackId = INVALID_STACK_ID;
Jaewan Kim62338192016-02-25 10:00:05 -0800128 private ComponentName mPipComponentName;
129 private MediaController mPipMediaController;
Dongwon Kang353d8d72016-04-13 21:55:22 -0700130 private String[] mLastPackagesResourceGranted;
Jaewan Kim26c63562017-04-26 15:41:43 +0900131 private PipNotification mPipNotification;
Winson Chungb6de8722017-06-02 12:45:51 -0700132 private ParceledListSlice mCustomActions;
Youngsang Choad8ceb02016-01-15 16:59:27 -0800133
Hongwei Wang43a752b2019-09-17 20:20:30 +0000134 // Used to calculate the movement bounds
135 private final DisplayInfo mTmpDisplayInfo = new DisplayInfo();
136 private final Rect mTmpInsetBounds = new Rect();
137 private final Rect mTmpNormalBounds = new Rect();
138
Winson Chung44315c62017-08-24 13:35:24 -0700139 // Keeps track of the IME visibility to adjust the PiP when the IME is visible
140 private boolean mImeVisible;
141 private int mImeHeightAdjustment;
142
Hongwei Wang43a752b2019-09-17 20:20:30 +0000143 private final PinnedStackListener mPinnedStackListener = new PipManagerPinnedStackListener();
Winson Chung2a82fe52017-02-02 14:43:34 -0800144
Wale Ogunwale480dca02016-02-06 13:58:29 -0800145 private final Runnable mResizePinnedStackRunnable = new Runnable() {
146 @Override
147 public void run() {
Winson Chung0a657372017-05-12 15:04:54 -0700148 resizePinnedStack(mResumeResizePinnedStackRunnableState);
Wale Ogunwale480dca02016-02-06 13:58:29 -0800149 }
150 };
Jaewan Kimf40fcdc2016-03-04 17:58:22 +0900151 private final Runnable mClosePipRunnable = new Runnable() {
152 @Override
153 public void run() {
154 closePip();
155 }
156 };
Youngsang Chof1647922015-12-17 13:39:39 -0800157
Youngsang Choad8ceb02016-01-15 16:59:27 -0800158 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
Youngsang Chof1647922015-12-17 13:39:39 -0800159 @Override
160 public void onReceive(Context context, Intent intent) {
Youngsang Choad8ceb02016-01-15 16:59:27 -0800161 String action = intent.getAction();
Jaewan Kimc552b042016-01-18 16:08:45 +0900162 if (Intent.ACTION_MEDIA_RESOURCE_GRANTED.equals(action)) {
Youngsang Choad8ceb02016-01-15 16:59:27 -0800163 String[] packageNames = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES);
164 int resourceType = intent.getIntExtra(Intent.EXTRA_MEDIA_RESOURCE_TYPE,
165 INVALID_RESOURCE_TYPE);
Dongwon Kang353d8d72016-04-13 21:55:22 -0700166 if (packageNames != null && packageNames.length > 0
Youngsang Choad8ceb02016-01-15 16:59:27 -0800167 && resourceType == Intent.EXTRA_MEDIA_RESOURCE_TYPE_VIDEO_CODEC) {
168 handleMediaResourceGranted(packageNames);
169 }
Youngsang Chof1647922015-12-17 13:39:39 -0800170 }
Youngsang Choad8ceb02016-01-15 16:59:27 -0800171
Youngsang Chof1647922015-12-17 13:39:39 -0800172 }
173 };
Jaewan Kim62338192016-02-25 10:00:05 -0800174 private final MediaSessionManager.OnActiveSessionsChangedListener mActiveMediaSessionListener =
175 new MediaSessionManager.OnActiveSessionsChangedListener() {
176 @Override
177 public void onActiveSessionsChanged(List<MediaController> controllers) {
178 updateMediaController(controllers);
179 }
180 };
Youngsang Chof1647922015-12-17 13:39:39 -0800181
Winson Chung2a82fe52017-02-02 14:43:34 -0800182 /**
183 * Handler for messages from the PIP controller.
184 */
Hongwei Wang43a752b2019-09-17 20:20:30 +0000185 private class PipManagerPinnedStackListener extends PinnedStackListener {
Winson Chung2a82fe52017-02-02 14:43:34 -0800186 @Override
Winson Chung44315c62017-08-24 13:35:24 -0700187 public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
188 if (mState == STATE_PIP) {
189 if (mImeVisible != imeVisible) {
190 if (imeVisible) {
191 // Save the IME height adjustment, and offset to not occlude the IME
192 mPipBounds.offset(0, -imeHeight);
193 mImeHeightAdjustment = imeHeight;
194 } else {
195 // Apply the inverse adjustment when the IME is hidden
196 mPipBounds.offset(0, mImeHeightAdjustment);
197 }
198 mImeVisible = imeVisible;
199 resizePinnedStack(STATE_PIP);
200 }
201 }
202 }
Winson Chung2a82fe52017-02-02 14:43:34 -0800203
204 @Override
Hongwei Wang43a752b2019-09-17 20:20:30 +0000205 public void onMovementBoundsChanged(Rect animatingBounds, boolean fromImeAdjustment,
206 boolean fromShelfAdjustment) {
Winson Chung2a82fe52017-02-02 14:43:34 -0800207 mHandler.post(() -> {
Hongwei Wang43a752b2019-09-17 20:20:30 +0000208 // Populate the inset / normal bounds and DisplayInfo from mPipBoundsHandler first.
209 mPipBoundsHandler.onMovementBoundsChanged(mTmpInsetBounds, mTmpNormalBounds,
210 animatingBounds, mTmpDisplayInfo);
211 mDefaultPipBounds.set(animatingBounds);
Winson Chung2a82fe52017-02-02 14:43:34 -0800212 });
213 }
214
215 @Override
Winson Chungb6de8722017-06-02 12:45:51 -0700216 public void onActionsChanged(ParceledListSlice actions) {
217 mCustomActions = actions;
218 mHandler.post(() -> {
219 for (int i = mListeners.size() - 1; i >= 0; --i) {
220 mListeners.get(i).onPipMenuActionsChanged(mCustomActions);
221 }
222 });
223 }
Winson Chung2a82fe52017-02-02 14:43:34 -0800224 }
225
Youngsang Chof1647922015-12-17 13:39:39 -0800226 private PipManager() { }
227
228 /**
229 * Initializes {@link PipManager}.
230 */
Fabian Kozynski5ca7a512019-10-16 19:56:11 +0000231 public void initialize(Context context, BroadcastDispatcher broadcastDispatcher) {
Youngsang Chof1647922015-12-17 13:39:39 -0800232 if (mInitialized) {
233 return;
234 }
235 mInitialized = true;
236 mContext = context;
Hongwei Wang43a752b2019-09-17 20:20:30 +0000237 mPipBoundsHandler = new PipBoundsHandler(context);
Wale Ogunwale65ebd952018-04-25 15:41:44 -0700238 mActivityTaskManager = ActivityTaskManager.getService();
Winson Chung2cf6ad82017-11-09 17:36:59 -0800239 ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
Jaewan Kim73ef3512016-07-18 13:50:33 +0900240 IntentFilter intentFilter = new IntentFilter();
241 intentFilter.addAction(Intent.ACTION_MEDIA_RESOURCE_GRANTED);
Fabian Kozynski5ca7a512019-10-16 19:56:11 +0000242 broadcastDispatcher.registerReceiver(mBroadcastReceiver, intentFilter,
243 null /* handler */, UserHandle.ALL);
Jaewan Kim73ef3512016-07-18 13:50:33 +0900244
Jaewan Kim8af250e2017-02-14 19:12:01 +0900245 if (sSettingsPackageAndClassNamePairList == null) {
246 String[] settings = mContext.getResources().getStringArray(
247 R.array.tv_pip_settings_class_name);
248 sSettingsPackageAndClassNamePairList = new ArrayList<>();
249 if (settings != null) {
250 for (int i = 0; i < settings.length; i++) {
251 Pair<String, String> entry = null;
252 String[] packageAndClassName =
253 settings[i].split(SETTINGS_PACKAGE_AND_CLASS_DELIMITER);
254 switch (packageAndClassName.length) {
255 case 1:
256 entry = Pair.<String, String>create(packageAndClassName[0], null);
257 break;
258 case 2:
Shuichi.Noguchid2067822018-01-13 21:47:04 +0900259 if (packageAndClassName[1] != null) {
260 entry = Pair.<String, String>create(packageAndClassName[0],
261 packageAndClassName[1].startsWith(".")
262 ? packageAndClassName[0] + packageAndClassName[1]
263 : packageAndClassName[1]);
Jaewan Kimdc298752017-02-24 10:45:12 +0900264 }
Shuichi.Noguchid2067822018-01-13 21:47:04 +0900265 break;
Jaewan Kim8af250e2017-02-14 19:12:01 +0900266 }
267 if (entry != null) {
268 sSettingsPackageAndClassNamePairList.add(entry);
Jaewan Kimdc298752017-02-24 10:45:12 +0900269 } else {
270 Log.w(TAG, "Ignoring malformed settings name " + settings[i]);
Jaewan Kim8af250e2017-02-14 19:12:01 +0900271 }
272 }
273 }
274 }
275
Winson Chung8068ec22017-06-02 09:58:00 -0700276 // Initialize the last orientation and apply the current configuration
277 Configuration initialConfig = mContext.getResources().getConfiguration();
278 mLastOrientation = initialConfig.orientation;
279 loadConfigurationsAndApply(initialConfig);
280
Jaewan Kim73ef3512016-07-18 13:50:33 +0900281 mMediaSessionManager =
282 (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
Winson Chung2a82fe52017-02-02 14:43:34 -0800283
284 try {
Hongwei Wang43a752b2019-09-17 20:20:30 +0000285 WindowManagerWrapper.getInstance().addPinnedStackListener(mPinnedStackListener);
Winson Chung2a82fe52017-02-02 14:43:34 -0800286 } catch (RemoteException e) {
287 Log.e(TAG, "Failed to register pinned stack listener", e);
288 }
Jaewan Kim26c63562017-04-26 15:41:43 +0900289
Fabian Kozynski5ca7a512019-10-16 19:56:11 +0000290 mPipNotification = new PipNotification(context, broadcastDispatcher);
Jaewan Kim73ef3512016-07-18 13:50:33 +0900291 }
292
Winson Chung0c5a5922017-05-22 17:41:06 -0700293 private void loadConfigurationsAndApply(Configuration newConfig) {
294 if (mLastOrientation != newConfig.orientation) {
295 // Don't resize the pinned stack on orientation change. TV does not care about this case
296 // and this could clobber the existing animation to the new bounds calculated by WM.
297 mLastOrientation = newConfig.orientation;
298 return;
299 }
300
Jaewan Kim73ef3512016-07-18 13:50:33 +0900301 Resources res = mContext.getResources();
Jaewan Kimd89a6942016-04-08 11:41:45 +0900302 mSettingsPipBounds = Rect.unflattenFromString(res.getString(
303 R.string.pip_settings_bounds));
Jaewan Kimc92a7d12016-02-15 17:33:25 -0800304 mMenuModePipBounds = Rect.unflattenFromString(res.getString(
Jaewan Kim8f584b82016-03-22 22:16:59 +0900305 R.string.pip_menu_bounds));
Youngsang Chof1647922015-12-17 13:39:39 -0800306
Jaewan Kim73ef3512016-07-18 13:50:33 +0900307 // Reset the PIP bounds and apply. PIP bounds can be changed by two reasons.
308 // 1. Configuration changed due to the language change (RTL <-> RTL)
309 // 2. SystemUI restarts after the crash
310 mPipBounds = isSettingsShown() ? mSettingsPipBounds : mDefaultPipBounds;
Jaewan Kime6309232017-04-14 15:26:20 +0900311 resizePinnedStack(getPinnedStackInfo() == null ? STATE_NO_PIP : STATE_PIP);
Youngsang Chof1647922015-12-17 13:39:39 -0800312 }
313
Jaewan Kimc552b042016-01-18 16:08:45 +0900314 /**
Jaewan Kim2d4d07c2016-05-20 10:28:17 +0900315 * Updates the PIP per configuration changed.
316 */
Winson Chung0c5a5922017-05-22 17:41:06 -0700317 public void onConfigurationChanged(Configuration newConfig) {
318 loadConfigurationsAndApply(newConfig);
Jaewan Kim26c63562017-04-26 15:41:43 +0900319 mPipNotification.onConfigurationChanged(mContext);
Jaewan Kim2d4d07c2016-05-20 10:28:17 +0900320 }
321
322 /**
Jaewan Kimf0fd2182016-04-20 21:17:58 +0900323 * Shows the picture-in-picture menu if an activity is in picture-in-picture mode.
Jaewan Kimc552b042016-01-18 16:08:45 +0900324 */
Winson Chungac52f282017-03-30 14:44:52 -0700325 public void showPictureInPictureMenu() {
Winson Chung0a657372017-05-12 15:04:54 -0700326 if (getState() == STATE_PIP) {
Wale Ogunwale480dca02016-02-06 13:58:29 -0800327 resizePinnedStack(STATE_PIP_MENU);
Jaewan Kimc552b042016-01-18 16:08:45 +0900328 }
329 }
330
Youngsang Chof1647922015-12-17 13:39:39 -0800331 /**
Jaewan Kim977dcdc2016-01-20 19:21:08 +0900332 * Closes PIP (PIPed activity and PIP system UI).
Youngsang Chof1647922015-12-17 13:39:39 -0800333 */
334 public void closePip() {
Youngsang Cho336007b2016-02-22 11:17:29 -0800335 closePipInternal(true);
Youngsang Cho23df6992016-01-26 17:51:33 -0800336 }
337
Youngsang Cho336007b2016-02-22 11:17:29 -0800338 private void closePipInternal(boolean removePipStack) {
Youngsang Chof1647922015-12-17 13:39:39 -0800339 mState = STATE_NO_PIP;
Youngsang Choad8ceb02016-01-15 16:59:27 -0800340 mPipTaskId = TASK_ID_NO_PIP;
Jaewan Kim62338192016-02-25 10:00:05 -0800341 mPipMediaController = null;
342 mMediaSessionManager.removeOnActiveSessionsChangedListener(mActiveMediaSessionListener);
Youngsang Cho23df6992016-01-26 17:51:33 -0800343 if (removePipStack) {
344 try {
Wale Ogunwale04d9cb52018-04-30 13:55:07 -0700345 mActivityTaskManager.removeStack(mPinnedStackId);
Youngsang Cho23df6992016-01-26 17:51:33 -0800346 } catch (RemoteException e) {
347 Log.e(TAG, "removeStack failed", e);
Wale Ogunwale89be5762017-10-04 13:27:49 -0700348 } finally {
349 mPinnedStackId = INVALID_STACK_ID;
Youngsang Cho23df6992016-01-26 17:51:33 -0800350 }
Youngsang Chof1647922015-12-17 13:39:39 -0800351 }
Jaewan Kimc92a7d12016-02-15 17:33:25 -0800352 for (int i = mListeners.size() - 1; i >= 0; --i) {
353 mListeners.get(i).onPipActivityClosed();
354 }
Jaewan Kimf40fcdc2016-03-04 17:58:22 +0900355 mHandler.removeCallbacks(mClosePipRunnable);
Jaewan Kimf0fd2182016-04-20 21:17:58 +0900356 updatePipVisibility(false);
Youngsang Chof1647922015-12-17 13:39:39 -0800357 }
358
359 /**
Jaewan Kim977dcdc2016-01-20 19:21:08 +0900360 * Moves the PIPed activity to the fullscreen and closes PIP system UI.
Youngsang Chof1647922015-12-17 13:39:39 -0800361 */
Jaewan Kima0d4d252016-03-31 13:37:10 +0900362 void movePipToFullscreen() {
Youngsang Choad8ceb02016-01-15 16:59:27 -0800363 mPipTaskId = TASK_ID_NO_PIP;
Jaewan Kimb0033642016-04-22 18:41:37 +0900364 for (int i = mListeners.size() - 1; i >= 0; --i) {
365 mListeners.get(i).onMoveToFullscreen();
366 }
Winson Chungf04c1dd2017-02-24 16:47:37 -0800367 resizePinnedStack(STATE_NO_PIP);
Jaewan Kim47fe2862016-10-10 16:23:28 +0900368 updatePipVisibility(false);
Youngsang Chof1647922015-12-17 13:39:39 -0800369 }
370
371 /**
Wale Ogunwale480dca02016-02-06 13:58:29 -0800372 * Suspends resizing operation on the Pip until {@link #resumePipResizing} is called
373 * @param reason The reason for suspending resizing operations on the Pip.
374 */
375 public void suspendPipResizing(int reason) {
376 if (DEBUG) Log.d(TAG,
377 "suspendPipResizing() reason=" + reason + " callers=" + Debug.getCallers(2));
378 mSuspendPipResizingReason |= reason;
379 }
380
381 /**
382 * Resumes resizing operation on the Pip that was previously suspended.
383 * @param reason The reason resizing operations on the Pip was suspended.
384 */
385 public void resumePipResizing(int reason) {
386 if ((mSuspendPipResizingReason & reason) == 0) {
387 return;
388 }
389 if (DEBUG) Log.d(TAG,
390 "resumePipResizing() reason=" + reason + " callers=" + Debug.getCallers(2));
391 mSuspendPipResizingReason &= ~reason;
392 mHandler.post(mResizePinnedStackRunnable);
393 }
394
395 /**
396 * Resize the Pip to the appropriate size for the input state.
397 * @param state In Pip state also used to determine the new size for the Pip.
398 */
Jaewan Kima0d4d252016-03-31 13:37:10 +0900399 void resizePinnedStack(int state) {
Jaewan Kim26c63562017-04-26 15:41:43 +0900400 if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + state, new Exception());
Winson Chungf04c1dd2017-02-24 16:47:37 -0800401 boolean wasStateNoPip = (mState == STATE_NO_PIP);
Wale Ogunwale480dca02016-02-06 13:58:29 -0800402 for (int i = mListeners.size() - 1; i >= 0; --i) {
403 mListeners.get(i).onPipResizeAboutToStart();
404 }
Wale Ogunwale480dca02016-02-06 13:58:29 -0800405 if (mSuspendPipResizingReason != 0) {
Winson Chung0a657372017-05-12 15:04:54 -0700406 mResumeResizePinnedStackRunnableState = state;
Winson Chungd62acec2017-03-31 19:42:32 -0700407 if (DEBUG) Log.d(TAG, "resizePinnedStack() deferring"
408 + " mSuspendPipResizingReason=" + mSuspendPipResizingReason
Winson Chung0a657372017-05-12 15:04:54 -0700409 + " mResumeResizePinnedStackRunnableState="
410 + mResumeResizePinnedStackRunnableState);
Wale Ogunwale480dca02016-02-06 13:58:29 -0800411 return;
412 }
Winson Chungd62acec2017-03-31 19:42:32 -0700413 mState = state;
Jaewan Kimc92a7d12016-02-15 17:33:25 -0800414 switch (mState) {
415 case STATE_NO_PIP:
416 mCurrentPipBounds = null;
Winson Chungf04c1dd2017-02-24 16:47:37 -0800417 // If the state was already STATE_NO_PIP, then do not resize the stack below as it
418 // will not exist
419 if (wasStateNoPip) {
420 return;
421 }
Jaewan Kimc92a7d12016-02-15 17:33:25 -0800422 break;
423 case STATE_PIP_MENU:
424 mCurrentPipBounds = mMenuModePipBounds;
425 break;
Jaewan Kime6309232017-04-14 15:26:20 +0900426 case STATE_PIP:
Jaewan Kima0d4d252016-03-31 13:37:10 +0900427 mCurrentPipBounds = mPipBounds;
428 break;
Jaewan Kimc92a7d12016-02-15 17:33:25 -0800429 default:
430 mCurrentPipBounds = mPipBounds;
431 break;
432 }
Winson Chungf04c1dd2017-02-24 16:47:37 -0800433 try {
434 int animationDurationMs = -1;
Evan Roskydbe2ce52019-07-18 11:13:17 -0700435 mActivityTaskManager.animateResizePinnedStack(mPinnedStackId, mCurrentPipBounds,
436 animationDurationMs);
Winson Chungf04c1dd2017-02-24 16:47:37 -0800437 } catch (RemoteException e) {
438 Log.e(TAG, "resizeStack failed", e);
Wale Ogunwale480dca02016-02-06 13:58:29 -0800439 }
440 }
441
442 /**
Winson Chung0a657372017-05-12 15:04:54 -0700443 * @return the current state, or the pending state if the state change was previously suspended.
444 */
445 private int getState() {
446 if (mSuspendPipResizingReason != 0) {
447 return mResumeResizePinnedStackRunnableState;
448 }
449 return mState;
450 }
451
452 /**
Jaewan Kima0d4d252016-03-31 13:37:10 +0900453 * Returns the default PIP bound.
Jaewan Kimc92a7d12016-02-15 17:33:25 -0800454 */
455 public Rect getPipBounds() {
Jaewan Kima0d4d252016-03-31 13:37:10 +0900456 return mPipBounds;
Jaewan Kimc92a7d12016-02-15 17:33:25 -0800457 }
458
459 /**
Youngsang Chof1647922015-12-17 13:39:39 -0800460 * Shows PIP menu UI by launching {@link PipMenuActivity}. It also locates the pinned
Jaewan Kim8f584b82016-03-22 22:16:59 +0900461 * stack to the centered PIP bound {@link R.config_centeredPictureInPictureBounds}.
Youngsang Chof1647922015-12-17 13:39:39 -0800462 */
Wale Ogunwale480dca02016-02-06 13:58:29 -0800463 private void showPipMenu() {
Youngsang Chof1647922015-12-17 13:39:39 -0800464 if (DEBUG) Log.d(TAG, "showPipMenu()");
Youngsang Chof1647922015-12-17 13:39:39 -0800465 mState = STATE_PIP_MENU;
466 for (int i = mListeners.size() - 1; i >= 0; --i) {
467 mListeners.get(i).onShowPipMenu();
468 }
469 Intent intent = new Intent(mContext, PipMenuActivity.class);
470 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Winson Chungb6de8722017-06-02 12:45:51 -0700471 intent.putExtra(PipMenuActivity.EXTRA_CUSTOM_ACTIONS, mCustomActions);
Jaewan Kim1a9dc562016-02-17 13:41:51 -0800472 mContext.startActivity(intent);
Youngsang Chof1647922015-12-17 13:39:39 -0800473 }
474
Jaewan Kima0d4d252016-03-31 13:37:10 +0900475 /**
476 * Adds a {@link Listener} to PipManager.
477 */
Youngsang Chof1647922015-12-17 13:39:39 -0800478 public void addListener(Listener listener) {
479 mListeners.add(listener);
480 }
481
Jaewan Kima0d4d252016-03-31 13:37:10 +0900482 /**
483 * Removes a {@link Listener} from PipManager.
484 */
Youngsang Chof1647922015-12-17 13:39:39 -0800485 public void removeListener(Listener listener) {
486 mListeners.remove(listener);
487 }
488
Jaewan Kima0d4d252016-03-31 13:37:10 +0900489 /**
490 * Adds a {@link MediaListener} to PipManager.
491 */
492 public void addMediaListener(MediaListener listener) {
493 mMediaListeners.add(listener);
494 }
495
496 /**
497 * Removes a {@link MediaListener} from PipManager.
498 */
499 public void removeMediaListener(MediaListener listener) {
500 mMediaListeners.remove(listener);
501 }
502
Jaewan Kimc92a7d12016-02-15 17:33:25 -0800503 /**
504 * Returns {@code true} if PIP is shown.
505 */
506 public boolean isPipShown() {
Jaewan Kim82ac50d2016-03-21 17:34:28 +0900507 return mState != STATE_NO_PIP;
Youngsang Chof1647922015-12-17 13:39:39 -0800508 }
509
Jaewan Kim73ef3512016-07-18 13:50:33 +0900510 private StackInfo getPinnedStackInfo() {
511 StackInfo stackInfo = null;
512 try {
Wale Ogunwale04d9cb52018-04-30 13:55:07 -0700513 stackInfo = ActivityTaskManager.getService().getStackInfo(
Wale Ogunwale68278562017-09-23 17:13:55 -0700514 WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
Jaewan Kim73ef3512016-07-18 13:50:33 +0900515 } catch (RemoteException e) {
516 Log.e(TAG, "getStackInfo failed", e);
517 }
518 return stackInfo;
519 }
520
Youngsang Choad8ceb02016-01-15 16:59:27 -0800521 private void handleMediaResourceGranted(String[] packageNames) {
Winson Chung0a657372017-05-12 15:04:54 -0700522 if (getState() == STATE_NO_PIP) {
Dongwon Kang353d8d72016-04-13 21:55:22 -0700523 mLastPackagesResourceGranted = packageNames;
524 } else {
525 boolean requestedFromLastPackages = false;
526 if (mLastPackagesResourceGranted != null) {
527 for (String packageName : mLastPackagesResourceGranted) {
528 for (String newPackageName : packageNames) {
529 if (TextUtils.equals(newPackageName, packageName)) {
530 requestedFromLastPackages = true;
531 break;
532 }
Youngsang Choad8ceb02016-01-15 16:59:27 -0800533 }
534 }
535 }
Dongwon Kang353d8d72016-04-13 21:55:22 -0700536 mLastPackagesResourceGranted = packageNames;
537 if (!requestedFromLastPackages) {
538 closePip();
539 }
Youngsang Choad8ceb02016-01-15 16:59:27 -0800540 }
541 }
542
Jaewan Kim62338192016-02-25 10:00:05 -0800543 private void updateMediaController(List<MediaController> controllers) {
544 MediaController mediaController = null;
Winson Chung0a657372017-05-12 15:04:54 -0700545 if (controllers != null && getState() != STATE_NO_PIP && mPipComponentName != null) {
Jaewan Kim62338192016-02-25 10:00:05 -0800546 for (int i = controllers.size() - 1; i >= 0; i--) {
547 MediaController controller = controllers.get(i);
548 // We assumes that an app with PIPable activity
549 // keeps the single instance of media controller especially when PIP is on.
550 if (controller.getPackageName().equals(mPipComponentName.getPackageName())) {
551 mediaController = controller;
552 break;
553 }
554 }
555 }
556 if (mPipMediaController != mediaController) {
557 mPipMediaController = mediaController;
Jaewan Kima0d4d252016-03-31 13:37:10 +0900558 for (int i = mMediaListeners.size() - 1; i >= 0; i--) {
559 mMediaListeners.get(i).onMediaControllerChanged();
Jaewan Kim62338192016-02-25 10:00:05 -0800560 }
Jaewan Kimf40fcdc2016-03-04 17:58:22 +0900561 if (mPipMediaController == null) {
562 mHandler.postDelayed(mClosePipRunnable,
563 CLOSE_PIP_WHEN_MEDIA_SESSION_GONE_TIMEOUT_MS);
564 } else {
565 mHandler.removeCallbacks(mClosePipRunnable);
566 }
Jaewan Kim62338192016-02-25 10:00:05 -0800567 }
568 }
569
570 /**
571 * Gets the {@link android.media.session.MediaController} for the PIPed activity.
572 */
573 MediaController getMediaController() {
574 return mPipMediaController;
575 }
576
Jaewan Kim8f584b82016-03-22 22:16:59 +0900577 /**
578 * Returns the PIPed activity's playback state.
Jaewan Kim26c63562017-04-26 15:41:43 +0900579 * This returns one of {@link #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED},
580 * or {@link #PLAYBACK_STATE_UNAVAILABLE}.
Jaewan Kim8f584b82016-03-22 22:16:59 +0900581 */
582 int getPlaybackState() {
583 if (mPipMediaController == null || mPipMediaController.getPlaybackState() == null) {
584 return PLAYBACK_STATE_UNAVAILABLE;
585 }
586 int state = mPipMediaController.getPlaybackState().getState();
587 boolean isPlaying = (state == PlaybackState.STATE_BUFFERING
588 || state == PlaybackState.STATE_CONNECTING
589 || state == PlaybackState.STATE_PLAYING
590 || state == PlaybackState.STATE_FAST_FORWARDING
591 || state == PlaybackState.STATE_REWINDING
592 || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS
593 || state == PlaybackState.STATE_SKIPPING_TO_NEXT);
594 long actions = mPipMediaController.getPlaybackState().getActions();
595 if (!isPlaying && ((actions & PlaybackState.ACTION_PLAY) != 0)) {
596 return PLAYBACK_STATE_PAUSED;
597 } else if (isPlaying && ((actions & PlaybackState.ACTION_PAUSE) != 0)) {
598 return PLAYBACK_STATE_PLAYING;
599 }
600 return PLAYBACK_STATE_UNAVAILABLE;
601 }
602
Jaewan Kim73ef3512016-07-18 13:50:33 +0900603 private boolean isSettingsShown() {
604 List<RunningTaskInfo> runningTasks;
605 try {
Wale Ogunwale04d9cb52018-04-30 13:55:07 -0700606 runningTasks = mActivityTaskManager.getTasks(1);
Winson Chung61c9e5a2017-10-11 10:39:32 -0700607 if (runningTasks.isEmpty()) {
Jaewan Kim73ef3512016-07-18 13:50:33 +0900608 return false;
609 }
610 } catch (RemoteException e) {
611 Log.d(TAG, "Failed to detect top activity", e);
612 return false;
613 }
614 ComponentName topActivity = runningTasks.get(0).topActivity;
Jaewan Kimd89a6942016-04-08 11:41:45 +0900615 for (Pair<String, String> componentName : sSettingsPackageAndClassNamePairList) {
616 String packageName = componentName.first;
Andrii Kulian8290f8f2016-06-30 17:51:32 -0700617 if (topActivity.getPackageName().equals(packageName)) {
Jaewan Kimd89a6942016-04-08 11:41:45 +0900618 String className = componentName.second;
619 if (className == null || topActivity.getClassName().equals(className)) {
620 return true;
621 }
622 }
623 }
624 return false;
625 }
626
Winson Chung67f5c8b2018-09-24 12:09:19 -0700627 private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
Youngsang Chof1647922015-12-17 13:39:39 -0800628 @Override
Jaewan Kim938a50b2016-03-14 17:35:43 +0900629 public void onTaskStackChanged() {
Jaewan Kim17ca4e32016-10-31 13:55:43 +0900630 if (DEBUG) Log.d(TAG, "onTaskStackChanged()");
Shuichi.Noguchie9d2dd62017-11-22 17:24:17 +0900631
Winson Chung0a657372017-05-12 15:04:54 -0700632 if (getState() != STATE_NO_PIP) {
Jaewan Kimd89a6942016-04-08 11:41:45 +0900633 boolean hasPip = false;
634
Jaewan Kim73ef3512016-07-18 13:50:33 +0900635 StackInfo stackInfo = getPinnedStackInfo();
636 if (stackInfo == null || stackInfo.taskIds == null) {
637 Log.w(TAG, "There is nothing in pinned stack");
638 closePipInternal(false);
Jaewan Kim938a50b2016-03-14 17:35:43 +0900639 return;
640 }
641 for (int i = stackInfo.taskIds.length - 1; i >= 0; --i) {
642 if (stackInfo.taskIds[i] == mPipTaskId) {
643 // PIP task is still alive.
Jaewan Kimd89a6942016-04-08 11:41:45 +0900644 hasPip = true;
645 break;
Jaewan Kim938a50b2016-03-14 17:35:43 +0900646 }
647 }
Jaewan Kimd89a6942016-04-08 11:41:45 +0900648 if (!hasPip) {
649 // PIP task doesn't exist anymore in PINNED_STACK.
650 closePipInternal(true);
651 return;
652 }
653 }
Winson Chung0a657372017-05-12 15:04:54 -0700654 if (getState() == STATE_PIP) {
Jaewan Kim73ef3512016-07-18 13:50:33 +0900655 Rect bounds = isSettingsShown() ? mSettingsPipBounds : mDefaultPipBounds;
656 if (mPipBounds != bounds) {
657 mPipBounds = bounds;
Jaewan Kime6309232017-04-14 15:26:20 +0900658 resizePinnedStack(STATE_PIP);
Jaewan Kimd89a6942016-04-08 11:41:45 +0900659 }
Jaewan Kim938a50b2016-03-14 17:35:43 +0900660 }
Youngsang Chof1647922015-12-17 13:39:39 -0800661 }
662
663 @Override
Wale Ogunwale89be5762017-10-04 13:27:49 -0700664 public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
Wale Ogunwale480dca02016-02-06 13:58:29 -0800665 if (DEBUG) Log.d(TAG, "onActivityPinned()");
Shuichi.Noguchie9d2dd62017-11-22 17:24:17 +0900666
Jaewan Kim73ef3512016-07-18 13:50:33 +0900667 StackInfo stackInfo = getPinnedStackInfo();
668 if (stackInfo == null) {
669 Log.w(TAG, "Cannot find pinned stack");
Jaewan Kim938a50b2016-03-14 17:35:43 +0900670 return;
671 }
672 if (DEBUG) Log.d(TAG, "PINNED_STACK:" + stackInfo);
Wale Ogunwale89be5762017-10-04 13:27:49 -0700673 mPinnedStackId = stackInfo.stackId;
Jaewan Kim938a50b2016-03-14 17:35:43 +0900674 mPipTaskId = stackInfo.taskIds[stackInfo.taskIds.length - 1];
675 mPipComponentName = ComponentName.unflattenFromString(
676 stackInfo.taskNames[stackInfo.taskNames.length - 1]);
Jaewan Kime6309232017-04-14 15:26:20 +0900677 // Set state to STATE_PIP so we show it when the pinned stack animation ends.
678 mState = STATE_PIP;
Jaewan Kim938a50b2016-03-14 17:35:43 +0900679 mCurrentPipBounds = mPipBounds;
Jaewan Kim938a50b2016-03-14 17:35:43 +0900680 mMediaSessionManager.addOnActiveSessionsChangedListener(
681 mActiveMediaSessionListener, null);
682 updateMediaController(mMediaSessionManager.getActiveSessions(null));
Jaewan Kim938a50b2016-03-14 17:35:43 +0900683 for (int i = mListeners.size() - 1; i >= 0; i--) {
684 mListeners.get(i).onPipEntered();
685 }
Jaewan Kimf0fd2182016-04-20 21:17:58 +0900686 updatePipVisibility(true);
Youngsang Chof1647922015-12-17 13:39:39 -0800687 }
Wale Ogunwalecc25a8a2016-01-23 14:31:37 -0800688
689 @Override
Winson Chunge6385a22017-05-02 18:15:16 -0700690 public void onPinnedActivityRestartAttempt(boolean clearedTask) {
Wale Ogunwale480dca02016-02-06 13:58:29 -0800691 if (DEBUG) Log.d(TAG, "onPinnedActivityRestartAttempt()");
Shuichi.Noguchie9d2dd62017-11-22 17:24:17 +0900692
Jaewan Kim938a50b2016-03-14 17:35:43 +0900693 // If PIPed activity is launched again by Launcher or intent, make it fullscreen.
694 movePipToFullscreen();
Wale Ogunwalecc25a8a2016-01-23 14:31:37 -0800695 }
Wale Ogunwale480dca02016-02-06 13:58:29 -0800696
697 @Override
698 public void onPinnedStackAnimationEnded() {
699 if (DEBUG) Log.d(TAG, "onPinnedStackAnimationEnded()");
Shuichi.Noguchie9d2dd62017-11-22 17:24:17 +0900700
Winson Chung0a657372017-05-12 15:04:54 -0700701 switch (getState()) {
Jaewan Kim938a50b2016-03-14 17:35:43 +0900702 case STATE_PIP_MENU:
703 showPipMenu();
704 break;
705 }
Wale Ogunwale480dca02016-02-06 13:58:29 -0800706 }
Jaewan Kim938a50b2016-03-14 17:35:43 +0900707 };
Youngsang Chof1647922015-12-17 13:39:39 -0800708
709 /**
710 * A listener interface to receive notification on changes in PIP.
711 */
712 public interface Listener {
Jaewan Kim82ac50d2016-03-21 17:34:28 +0900713 /**
714 * Invoked when an activity is pinned and PIP manager is set corresponding information.
715 * Classes must use this instead of {@link android.app.ITaskStackListener.onActivityPinned}
716 * because there's no guarantee for the PIP manager be return relavent information
Jaewan Kima0d4d252016-03-31 13:37:10 +0900717 * correctly. (e.g. {@link isPipShown}).
Jaewan Kim82ac50d2016-03-21 17:34:28 +0900718 */
719 void onPipEntered();
Wale Ogunwale480dca02016-02-06 13:58:29 -0800720 /** Invoked when a PIPed activity is closed. */
Youngsang Chof1647922015-12-17 13:39:39 -0800721 void onPipActivityClosed();
Wale Ogunwale480dca02016-02-06 13:58:29 -0800722 /** Invoked when the PIP menu gets shown. */
Youngsang Chof1647922015-12-17 13:39:39 -0800723 void onShowPipMenu();
Winson Chungb6de8722017-06-02 12:45:51 -0700724 /** Invoked when the PIP menu actions change. */
725 void onPipMenuActionsChanged(ParceledListSlice actions);
Jaewan Kim10a86912016-04-04 16:01:51 +0900726 /** Invoked when the PIPed activity is about to return back to the fullscreen. */
Youngsang Chof1647922015-12-17 13:39:39 -0800727 void onMoveToFullscreen();
Wale Ogunwale480dca02016-02-06 13:58:29 -0800728 /** Invoked when we are above to start resizing the Pip. */
729 void onPipResizeAboutToStart();
Jaewan Kima0d4d252016-03-31 13:37:10 +0900730 }
731
732 /**
733 * A listener interface to receive change in PIP's media controller
734 */
735 public interface MediaListener {
Jaewan Kim62338192016-02-25 10:00:05 -0800736 /** Invoked when the MediaController on PIPed activity is changed. */
737 void onMediaControllerChanged();
Youngsang Chof1647922015-12-17 13:39:39 -0800738 }
739
740 /**
741 * Gets an instance of {@link PipManager}.
742 */
743 public static PipManager getInstance() {
744 if (sPipManager == null) {
745 sPipManager = new PipManager();
746 }
747 return sPipManager;
748 }
Jaewan Kima0d4d252016-03-31 13:37:10 +0900749
Winsonab216602016-08-09 14:05:20 -0700750 private void updatePipVisibility(final boolean visible) {
Winson Chung67f5c8b2018-09-24 12:09:19 -0700751 Dependency.get(UiOffloadThread.class).submit(() -> {
752 WindowManagerWrapper.getInstance().setPipVisibility(visible);
753 });
Jaewan Kimf0fd2182016-04-20 21:17:58 +0900754 }
Youngsang Chof1647922015-12-17 13:39:39 -0800755}