Use pause signaling to halt a video call when data limit is reached.
Tyler Gunn [Wed, 21 Dec 2016 18:39:48 +0000 (10:39 -0800)]
This proved to be problematic as the InCallUI is the only place where
we would previously get a pause signal.  Added a VideoPauseTracker
class which is responsible for tracking the source of pause requests and
ensuring that the video is only paused on the first request, and only
resumed on the last resume request.

Added some new logic to ImsVideoCallProviderWrapper to support receiving
pause and resume requests from other sources, and to ensure that requests
to pause or resume use the VideoPauseTracker to determine if the pause
or resume should actually be passed along to the modem.

Test: manual
Bug: 30760683
Change-Id: Id54b2a955745132ab09feb01b5c961f6115ef3df

src/java/com/android/ims/internal/ImsVideoCallProviderWrapper.java
src/java/com/android/ims/internal/VideoPauseTracker.java [new file with mode: 0644]

index 3f0e629..e2bc438 100644 (file)
@@ -24,6 +24,7 @@ import android.os.Message;
 import android.os.RegistrantList;
 import android.os.RemoteException;
 import android.telecom.Connection;
+import android.telecom.Log;
 import android.telecom.VideoProfile;
 import android.view.Surface;
 
@@ -64,6 +65,7 @@ public class ImsVideoCallProviderWrapper extends Connection.VideoProvider {
     private RegistrantList mDataUsageUpdateRegistrants = new RegistrantList();
     private final Set<ImsVideoProviderWrapperCallback> mCallbacks = Collections.newSetFromMap(
             new ConcurrentHashMap<ImsVideoProviderWrapperCallback, Boolean>(8, 0.9f, 1));
+    private VideoPauseTracker mVideoPauseTracker = new VideoPauseTracker();
 
     private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
         @Override
@@ -253,9 +255,29 @@ public class ImsVideoCallProviderWrapper extends Connection.VideoProvider {
         }
     }
 
-    /** @inheritDoc */
+    /**
+     * Handles session modify requests received from the {@link android.telecom.InCallService}.
+     *
+     * @inheritDoc
+     **/
     public void onSendSessionModifyRequest(VideoProfile fromProfile, VideoProfile toProfile) {
+        if (fromProfile == null || toProfile == null) {
+            Log.w(this, "onSendSessionModifyRequest: null profile in request.");
+            return;
+        }
+
         try {
+            toProfile = maybeFilterPauseResume(fromProfile, toProfile,
+                    VideoPauseTracker.SOURCE_INCALL);
+
+            int fromVideoState = fromProfile.getVideoState();
+            int toVideoState = toProfile.getVideoState();
+            Log.i(this, "onSendSessionModifyRequest: fromVideoState=%s, toVideoState=%s; ",
+                    VideoProfile.videoStateToString(fromProfile.getVideoState()),
+                    VideoProfile.videoStateToString(toProfile.getVideoState()));
+            if (fromVideoState == toVideoState) {
+                return;
+            }
             mVideoCallProvider.sendSessionModifyRequest(fromProfile, toProfile);
         } catch (RemoteException e) {
         }
@@ -292,4 +314,154 @@ public class ImsVideoCallProviderWrapper extends Connection.VideoProvider {
         } catch (RemoteException e) {
         }
     }
+
+    /**
+     * Determines if a session modify request represents a request to pause the video.
+     *
+     * @param from The from video state.
+     * @param to The to video state.
+     * @return {@code true} if a pause was requested.
+     */
+    private static boolean isPauseRequest(int from, int to) {
+        boolean fromPaused = VideoProfile.isPaused(from);
+        boolean toPaused = VideoProfile.isPaused(to);
+
+        return !fromPaused && toPaused;
+    }
+
+    /**
+     * Determines if a session modify request represents a request to resume the video.
+     *
+     * @param from The from video state.
+     * @param to The to video state.
+     * @return {@code true} if a resume was requested.
+     */
+    private static boolean isResumeRequest(int from, int to) {
+        boolean fromPaused = VideoProfile.isPaused(from);
+        boolean toPaused = VideoProfile.isPaused(to);
+
+        return fromPaused && !toPaused;
+    }
+
+    /**
+     * Filters incoming pause and resume requests based on whether there are other active pause or
+     * resume requests at the current time.
+     *
+     * Requests to pause the video stream using the {@link VideoProfile#STATE_PAUSED} bit can come
+     * from both the {@link android.telecom.InCallService}, as well as via the
+     * {@link #pauseVideo(int, int)} and {@link #resumeVideo(int, int)} methods.  As a result,
+     * multiple sources can potentially pause or resume the video stream.  This method ensures that
+     * providing any one request source has paused the video that the video will remain paused.
+     *
+     * @param fromProfile The request's from {@link VideoProfile}.
+     * @param toProfile The request's to {@link VideoProfile}.
+     * @param source The source of the request, as identified by a {@code VideoPauseTracker#SOURCE*}
+     *               constant.
+     * @return The new toProfile, with the pause bit set or unset based on whether we should
+     *      actually pause or resume the video at the current time.
+     */
+    private VideoProfile maybeFilterPauseResume(VideoProfile fromProfile, VideoProfile toProfile,
+            int source) {
+        int fromVideoState = fromProfile.getVideoState();
+        int toVideoState = toProfile.getVideoState();
+
+        // TODO: Remove the following workaround in favor of a new API.
+        // The current sendSessionModifyRequest API has a flaw.  If the video is already
+        // paused, it is not possible for the IncallService to inform the VideoProvider that
+        // it wishes to pause due to multi-tasking.
+        // In a future release we should add a new explicity pauseVideo and resumeVideo API
+        // instead of a difference between two video states.
+        // For now, we'll assume if the request is from pause to pause, we'll still try to
+        // pause.
+        boolean isPauseSpecialCase = (source == VideoPauseTracker.SOURCE_INCALL &&
+                VideoProfile.isPaused(fromVideoState) &&
+                VideoProfile.isPaused(toVideoState));
+
+        boolean isPauseRequest = isPauseRequest(fromVideoState, toVideoState) || isPauseSpecialCase;
+        boolean isResumeRequest = isResumeRequest(fromVideoState, toVideoState);
+        if (isPauseRequest) {
+            Log.i(this, "maybeFilterPauseResume: isPauseRequest");
+            // Check if we have already paused the video in the past.
+            if (!mVideoPauseTracker.shouldPauseVideoFor(source) && !isPauseSpecialCase) {
+                // Note: We don't want to remove the "pause" in the "special case" scenario. If we
+                // do the resulting request will be from PAUSED --> UNPAUSED, which would resume the
+                // video.
+
+                // Video was already paused, so remove the pause in the "to" profile.
+                toVideoState = toVideoState & ~VideoProfile.STATE_PAUSED;
+                toProfile = new VideoProfile(toVideoState, toProfile.getQuality());
+            }
+        } else if (isResumeRequest) {
+            Log.i(this, "maybeFilterPauseResume: isResumeRequest");
+            // Check if we should remain paused (other pause requests pending).
+            if (!mVideoPauseTracker.shouldResumeVideoFor(source)) {
+                // There are other pause requests from other sources which are still active, so we
+                // should remain paused.
+                toVideoState = toVideoState | VideoProfile.STATE_PAUSED;
+                toProfile = new VideoProfile(toVideoState, toProfile.getQuality());
+            }
+        }
+
+        return toProfile;
+    }
+
+    /**
+     * Issues a request to pause the video using {@link VideoProfile#STATE_PAUSED} from a source
+     * other than the InCall UI.
+     *
+     * @param fromVideoState The current video state (prior to issuing the pause).
+     * @param source The source of the pause request.
+     */
+    public void pauseVideo(int fromVideoState, int source) {
+        if (mVideoPauseTracker.shouldPauseVideoFor(source)) {
+            // We should pause the video (its not already paused).
+            VideoProfile fromProfile = new VideoProfile(fromVideoState);
+            VideoProfile toProfile = new VideoProfile(fromVideoState | VideoProfile.STATE_PAUSED);
+
+            try {
+                Log.i(this, "pauseVideo: fromVideoState=%s, toVideoState=%s",
+                        VideoProfile.videoStateToString(fromProfile.getVideoState()),
+                        VideoProfile.videoStateToString(toProfile.getVideoState()));
+                mVideoCallProvider.sendSessionModifyRequest(fromProfile, toProfile);
+            } catch (RemoteException e) {
+            }
+        } else {
+            Log.i(this, "pauseVideo: video already paused");
+        }
+    }
+
+    /**
+     * Issues a request to resume the video using {@link VideoProfile#STATE_PAUSED} from a source
+     * other than the InCall UI.
+     *
+     * @param fromVideoState The current video state (prior to issuing the resume).
+     * @param source The source of the resume request.
+     */
+    public void resumeVideo(int fromVideoState, int source) {
+        if (mVideoPauseTracker.shouldResumeVideoFor(source)) {
+            // We are the last source to resume, so resume now.
+            VideoProfile fromProfile = new VideoProfile(fromVideoState);
+            VideoProfile toProfile = new VideoProfile(fromVideoState & ~VideoProfile.STATE_PAUSED);
+
+            try {
+                Log.i(this, "resumeVideo: fromVideoState=%s, toVideoState=%s",
+                        VideoProfile.videoStateToString(fromProfile.getVideoState()),
+                        VideoProfile.videoStateToString(toProfile.getVideoState()));
+                mVideoCallProvider.sendSessionModifyRequest(fromProfile, toProfile);
+            } catch (RemoteException e) {
+            }
+        } else {
+            Log.i(this, "resumeVideo: remaining paused (paused from other sources)");
+        }
+    }
+
+    /**
+     * Determines if a specified source has issued a pause request.
+     *
+     * @param source The source.
+     * @return {@code true} if the source issued a pause request, {@code false} otherwise.
+     */
+    public boolean wasVideoPausedFromSource(int source) {
+        return mVideoPauseTracker.wasVideoPausedFromSource(source);
+    }
 }
diff --git a/src/java/com/android/ims/internal/VideoPauseTracker.java b/src/java/com/android/ims/internal/VideoPauseTracker.java
new file mode 100644 (file)
index 0000000..d37f7fa
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2016 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.ims.internal;
+
+import android.telecom.Log;
+import android.telecom.VideoProfile;
+import android.util.ArraySet;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.StringJoiner;
+import java.util.stream.Collectors;
+
+/**
+ * Used by an {@link ImsVideoCallProviderWrapper} to track requests to pause video from various
+ * sources.
+ *
+ * Requests to pause the video stream using the {@link VideoProfile#STATE_PAUSED} bit can come
+ * from both the {@link android.telecom.InCallService}, as well as via the
+ * {@link ImsVideoCallProviderWrapper#pauseVideo(int, int)} and
+ * {@link ImsVideoCallProviderWrapper#resumeVideo(int, int)} methods.  As a result, multiple sources
+ * can potentially pause or resume the video stream.
+ *
+ * This class is responsible for tracking any active requests to pause the video.
+ */
+public class VideoPauseTracker {
+    /** The pause or resume request originated from an InCallService. */
+    public static final int SOURCE_INCALL = 1;
+
+    /**
+     * The pause or resume request originated from a change to the data enabled state from the
+     * {@code ImsPhoneCallTracker#onDataEnabledChanged(boolean, int)} callback.  This happens when
+     * the user reaches their data limit or enables and disables data.
+     */
+    public static final int SOURCE_DATA_ENABLED = 2;
+
+    private static final String SOURCE_INCALL_STR = "INCALL";
+    private static final String SOURCE_DATA_ENABLED_STR = "DATA_ENABLED";
+
+    /**
+     * Tracks the current sources of pause requests.
+     */
+    private Set<Integer> mPauseRequests = new ArraySet<Integer>(2);
+
+    /**
+     * Lock for the {@link #mPauseRequests} {@link ArraySet}.
+     */
+    private Object mPauseRequestsLock = new Object();
+
+    /**
+     * Tracks a request to pause the video for a source (see {@link #SOURCE_DATA_ENABLED},
+     * {@link #SOURCE_INCALL}) and determines whether a pause request should be issued to the
+     * video provider.
+     *
+     * We want to issue a pause request to the provider when we receive the first request
+     * to pause via any source and we're not already paused.
+     *
+     * @param source The source of the pause request.
+     * @return {@code true} if a pause should be issued to the
+     *      {@link com.android.ims.internal.ImsVideoCallProvider}, {@code false} otherwise.
+     */
+    public boolean shouldPauseVideoFor(int source) {
+        synchronized (mPauseRequestsLock) {
+            boolean wasPaused = isPaused();
+            mPauseRequests.add(source);
+
+            if (!wasPaused) {
+                Log.i(this, "shouldPauseVideoFor: source=%s, pendingRequests=%s - should pause",
+                        sourceToString(source), sourcesToString(mPauseRequests));
+                // There were previously no pause requests, but there is one now, so pause.
+                return true;
+            } else {
+                Log.i(this, "shouldPauseVideoFor: source=%s, pendingRequests=%s - already paused",
+                        sourceToString(source), sourcesToString(mPauseRequests));
+                // There were already pause requests, so no need to re-pause.
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Tracks a request to resume the video for a source (see {@link #SOURCE_DATA_ENABLED},
+     * {@link #SOURCE_INCALL}) and determines whether a resume request should be issued to the
+     * video provider.
+     *
+     * We want to issue a resume request to the provider when we have issued a corresponding
+     * resume for each previously issued pause.
+     *
+     * @param source The source of the resume request.
+     * @return {@code true} if a resume should be issued to the
+     *      {@link com.android.ims.internal.ImsVideoCallProvider}, {@code false} otherwise.
+     */
+    public boolean shouldResumeVideoFor(int source) {
+        synchronized (mPauseRequestsLock) {
+            boolean wasPaused = isPaused();
+            mPauseRequests.remove(source);
+            boolean isPaused = isPaused();
+
+            if (wasPaused && !isPaused) {
+                Log.i(this, "shouldResumeVideoFor: source=%s, pendingRequests=%s - should resume",
+                        sourceToString(source), sourcesToString(mPauseRequests));
+                // This was the last pause request, so resume video.
+                return true;
+            } else if (wasPaused && isPaused) {
+                Log.i(this, "shouldResumeVideoFor: source=%s, pendingRequests=%s - stay paused",
+                        sourceToString(source), sourcesToString(mPauseRequests));
+                // There are still pending pause requests, so don't resume.
+                return false;
+            } else {
+                Log.i(this, "shouldResumeVideoFor: source=%s, pendingRequests=%s - not paused",
+                        sourceToString(source), sourcesToString(mPauseRequests));
+                // Video wasn't paused, so don't resume.
+                return false;
+            }
+        }
+    }
+
+    /**
+     * @return {@code true} if the video should be paused, {@code false} otherwise.
+     */
+    public boolean isPaused() {
+        synchronized (mPauseRequestsLock) {
+            return !mPauseRequests.isEmpty();
+        }
+    }
+
+    /**
+     * @param source the source of the pause.
+     * @return {@code true} if the specified source initiated a pause request and the video is
+     *      currently paused, {@code false} otherwise.
+     */
+    public boolean wasVideoPausedFromSource(int source) {
+        synchronized (mPauseRequestsLock) {
+            return mPauseRequests.contains(source);
+        }
+    }
+
+    /**
+     * Returns a string equivalent of a {@code SOURCE_*} constant.
+     *
+     * @param source A {@code SOURCE_*} constant.
+     * @return String equivalent of the source.
+     */
+    private String sourceToString(int source) {
+        switch (source) {
+            case SOURCE_DATA_ENABLED:
+                return SOURCE_DATA_ENABLED_STR;
+            case SOURCE_INCALL:
+                return SOURCE_INCALL_STR;
+        }
+        return "unknown";
+    }
+
+    /**
+     * Returns a comma separated list of sources.
+     *
+     * @param sources The sources.
+     * @return Comma separated list of sources.
+     */
+    private String sourcesToString(Collection<Integer> sources) {
+        synchronized (mPauseRequestsLock) {
+            return sources.stream()
+                    .map(source -> sourceToString(source))
+                    .collect(Collectors.joining(", "));
+        }
+    }
+}