IMS-VT: Send static image in a Video Call

1. Add "hide me" button that enables end user to send
   static image in a Video Call
2. Disable "Switch Camera" button, Zoom when video call
   is in "hide me"
3. Change "hide me" button text to "show me" when user
   clicks on "hide me" and vice versa
4. Open/Close the camera when user is in "show me"/"hide me"
5. Update the preview window with the static image when in
   "hide me".
6. Switch preview window to show video preview when in "show me"
7. Add <InCallEventListener> interface API,<onSendStaticImageStateChanged>
   that gets invoked whenever hide me user selection is changed.

Change-Id: Ic8cb669a912db754be493f9d3b1a691d1a081c9c
CRs-Fixed: 1080109
diff --git a/InCallUI/res/layout/call_button_fragment.xml b/InCallUI/res/layout/call_button_fragment.xml
index 9ed4e56..3bc306e 100644
--- a/InCallUI/res/layout/call_button_fragment.xml
+++ b/InCallUI/res/layout/call_button_fragment.xml
@@ -158,6 +158,13 @@
             android:contentDescription="@string/onscreenAddParticipant"
             android:visibility="gone" />
 
+        <!-- "Hide Me" -->
+        <ImageButton android:id="@+id/hideMe"
+            style="@style/InCallButton"
+            android:background="@drawable/btn_compound_video_off"
+            android:contentDescription="@string/qti_ims_hideMeText_unselected"
+            android:visibility="gone" />
+
         <!-- "Blind transfer" -->
         <ImageButton android:id="@+id/blindTransfer"
             style="@style/InCallButton"
diff --git a/InCallUI/res/values/qtistrings.xml b/InCallUI/res/values/qtistrings.xml
index eba800a..8f068a0 100644
--- a/InCallUI/res/values/qtistrings.xml
+++ b/InCallUI/res/values/qtistrings.xml
@@ -81,6 +81,10 @@
     <!-- Modify call error cause -->
     <string name="modify_call_failed_due_to_low_battery">Modify call failed due to low battery.</string>
 
+    <string name="qti_ims_hideMeText_unselected">Hide Me</string>
+    <string name="qti_ims_hideMeText_selected">Show Me</string>
+    <string name="qti_ims_defaultImage_fallback">Using default image</string>
+
     <!-- Description of the call transfer related strings [CHAR LIMIT=NONE] -->
     <string name="qti_ims_transfer_num_error">Number not set. Provide the number via IMS settings and retry.</string>
     <string name="qti_ims_transfer_request_error">Call Transfer request had failed.</string>
diff --git a/InCallUI/src/com/android/incallui/CallButtonFragment.java b/InCallUI/src/com/android/incallui/CallButtonFragment.java
index d03872b..e44df78 100644
--- a/InCallUI/src/com/android/incallui/CallButtonFragment.java
+++ b/InCallUI/src/com/android/incallui/CallButtonFragment.java
@@ -37,6 +37,7 @@
 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_RX_VIDEO_CALL;
 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_VO_VIDEO_CALL;
 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_ADD_PARTICIPANT;
+import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_HIDE_ME;
 
 import android.content.Context;
 import android.content.res.ColorStateList;
@@ -109,7 +110,8 @@
         public static final int BUTTON_RX_VIDEO_CALL = 17;
         public static final int BUTTON_VO_VIDEO_CALL = 18;
         public static final int BUTTON_ADD_PARTICIPANT = 19;
-        public static final int BUTTON_COUNT = 20;
+        public static final int BUTTON_HIDE_ME = 20;
+        public static final int BUTTON_COUNT = 21;
     }
 
     private SparseIntArray mButtonVisibilityMap = new SparseIntArray(BUTTON_COUNT);
@@ -131,6 +133,7 @@
     private ImageButton mAssuredTransferButton;
     private ImageButton mConsultativeTransferButton;
     private ImageButton mAddParticipantButton;
+    private ImageButton mHideMeButton;
     private ImageButton mRecordButton;
     private ImageButton mRxTxVideoCallButton;
     private ImageButton mRxVideoCallButton;
@@ -206,6 +209,8 @@
         mConsultativeTransferButton.setOnClickListener(this);
         mAddParticipantButton = (ImageButton) parent.findViewById(R.id.addParticipant);
         mAddParticipantButton.setOnClickListener(this);
+        mHideMeButton = (ImageButton) parent.findViewById(R.id.hideMe);
+        mHideMeButton.setOnClickListener(this);
         mOverflowButton = (ImageButton) parent.findViewById(R.id.overflowButton);
         mOverflowButton.setOnClickListener(this);
         mManageVideoCallConferenceButton = (ImageButton) parent.findViewById(
@@ -262,6 +267,8 @@
             getPresenter().showDialpadClicked(!mShowDialpadButton.isSelected());
         } else if (id == R.id.addParticipant) {
             getPresenter().addParticipantClicked();
+        } else if (id == R.id.hideMe) {
+            getPresenter().hideMeClicked(!mHideMeButton.isSelected());
         } else if (id == R.id.changeToVideoButton) {
             getPresenter().changeToVideoClicked();
         } else if (id == R.id.changeToVoiceButton) {
@@ -361,6 +368,7 @@
                 mChangeToVoiceButton,
                 mAddCallButton,
                 mMergeButton,
+                mHideMeButton,
                 mBlindTransferButton,
                 mAssuredTransferButton,
                 mConsultativeTransferButton,
@@ -463,6 +471,7 @@
         mOverflowButton.setEnabled(isEnabled);
         mManageVideoCallConferenceButton.setEnabled(isEnabled);
         mAddParticipantButton.setEnabled(isEnabled);
+        mHideMeButton.setEnabled(isEnabled);
         mRecordButton.setEnabled(isEnabled);
         mRxTxVideoCallButton.setEnabled(isEnabled);
         mRxVideoCallButton.setEnabled(isEnabled);
@@ -523,6 +532,8 @@
             return mRxVideoCallButton;
         } else if (id == BUTTON_VO_VIDEO_CALL) {
             return mVoVideoCallButton;
+        } else if (id == BUTTON_HIDE_ME) {
+            return mHideMeButton;
         } else {
             Log.w(this, "Invalid button id");
             return null;
@@ -565,6 +576,16 @@
         }
     }
 
+    @Override
+    public void setHideMe(boolean value) {
+        if (mHideMeButton.isSelected() != value) {
+            mHideMeButton.setSelected(value);
+            mHideMeButton.setContentDescription(getContext().getString(
+                    value ? R.string.qti_ims_hideMeText_selected
+                            : R.string.qti_ims_hideMeText_unselected));
+        }
+    }
+
     private void addToOverflowMenu(int id, View button, PopupMenu menu) {
         button.setVisibility(View.GONE);
         menu.getMenu().add(Menu.NONE, id, Menu.NONE, button.getContentDescription());
diff --git a/InCallUI/src/com/android/incallui/CallButtonPresenter.java b/InCallUI/src/com/android/incallui/CallButtonPresenter.java
index 665ed8b..6262faf 100644
--- a/InCallUI/src/com/android/incallui/CallButtonPresenter.java
+++ b/InCallUI/src/com/android/incallui/CallButtonPresenter.java
@@ -35,6 +35,7 @@
 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_RX_VIDEO_CALL;
 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_VO_VIDEO_CALL;
 import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_ADD_PARTICIPANT;
+import static com.android.incallui.CallButtonFragment.Buttons.BUTTON_HIDE_ME;
 
 import android.content.Context;
 import android.os.Build;
@@ -75,6 +76,8 @@
     private boolean mPreviousMuteState = false;
     private static final int MAX_PARTICIPANTS_LIMIT = 6;
     private boolean mEnhanceEnable = false;
+    // Holds TRUE if user clicked on "hideme" option else holds false
+    private static boolean sIsHideMe = false;
 
     // NOTE: Capability constant definition has been duplicated to avoid bundling
     // the Dialer with Frameworks.
@@ -145,6 +148,9 @@
             mCall = callList.getIncomingCall();
         } else {
             mCall = null;
+            if (newState == InCallState.NO_CALLS) {
+                sIsHideMe = false;
+            }
         }
         updateUi(newState, mCall);
     }
@@ -399,6 +405,22 @@
         }
     }
 
+    /**
+     * Handles click on hide me button
+     * @param isHideMe True if user selected hide me option else false
+     */
+    public void hideMeClicked(boolean isHideMe) {
+        sIsHideMe = isHideMe;
+
+        if (getUi() != null && mCall != null) {
+            updateButtonsState(mCall);
+        }
+
+        /* Click on hideme shall change the static image state i.e. decision
+           is made in VideoCallPresenter whether to replace preview video with
+           static image or whether to resume preview video streaming */
+        InCallPresenter.getInstance().notifyStaticImageStateChanged(isHideMe);
+    }
 
     /**
      * Stop or start client's video transmission.
@@ -551,7 +573,13 @@
         ui.showButton(BUTTON_ADD_CALL, showAddCall);
         ui.showButton(BUTTON_UPGRADE_TO_VIDEO, showUpgradeToVideo && !mEnhanceEnable);
         ui.showButton(BUTTON_DOWNGRADE_TO_AUDIO, showDowngradeToAudio && !useExt);
-        ui.showButton(BUTTON_SWITCH_CAMERA, isVideo);
+        Log.v(this, "updateButtonsState sIsHideMe: " + sIsHideMe);
+        ui.setHideMe(sIsHideMe);
+        // show switch camera button only if video call is NOT in hideme mode
+        ui.showButton(BUTTON_SWITCH_CAMERA, isVideo && !sIsHideMe);
+        // show hide me button only for active video calls
+        ui.showButton(BUTTON_HIDE_ME, isCallActive && isVideo &&
+                QtiCallUtils.shallTransmitStaticImage(getUi().getContext()));
         ui.showButton(BUTTON_PAUSE_VIDEO, isVideo && !useExt && !useCustomVideoUi &&
                 !mEnhanceEnable);
         if (isVideo) {
@@ -641,6 +669,7 @@
         void setEnabled(boolean on);
         void setMute(boolean on);
         void setHold(boolean on);
+        void setHideMe(boolean on);
         void setCameraSwitched(boolean isBackFacingCamera);
         void setVideoPaused(boolean isPaused);
         void setAudio(int mode);
diff --git a/InCallUI/src/com/android/incallui/CallCardPresenter.java b/InCallUI/src/com/android/incallui/CallCardPresenter.java
index 4317a5e..91b67f8 100644
--- a/InCallUI/src/com/android/incallui/CallCardPresenter.java
+++ b/InCallUI/src/com/android/incallui/CallCardPresenter.java
@@ -1169,6 +1169,11 @@
         updatePrimaryDisplayInfo();
     }
 
+    @Override
+    public void onSendStaticImageStateChanged(boolean isEnabled) {
+        //No-op
+    }
+
     private boolean isPrimaryCallActive() {
         return mPrimary != null && mPrimary.getState() == Call.State.ACTIVE;
     }
diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java
index 0acc35e..1e0a8f7 100644
--- a/InCallUI/src/com/android/incallui/InCallPresenter.java
+++ b/InCallUI/src/com/android/incallui/InCallPresenter.java
@@ -1376,6 +1376,17 @@
         }
     }
 
+   /**
+     * Called by the {@link CallButtonPresenter} to inform of a change in hide me selection.
+     *
+     * @param isEnabled {@code True} if entering hide me mode.
+     */
+    public void notifyStaticImageStateChanged(boolean isEnabled) {
+        for (InCallEventListener listener : mInCallEventListeners) {
+            listener.onSendStaticImageStateChanged(isEnabled);
+        }
+    }
+
     /**
      * Update  color of sim card icon
      */
@@ -2129,6 +2140,7 @@
         public void onSecondaryCallerInfoVisibilityChanged(boolean isVisible, int height);
         public void updatePrimaryCallState();
         public void onIncomingVideoAvailabilityChanged(boolean isAvailable);
+        public void onSendStaticImageStateChanged(boolean isEnabled);
     }
 
     public interface InCallUiListener {
diff --git a/InCallUI/src/com/android/incallui/QtiCallUtils.java b/InCallUI/src/com/android/incallui/QtiCallUtils.java
index 6a4383a..0e65bb4 100644
--- a/InCallUI/src/com/android/incallui/QtiCallUtils.java
+++ b/InCallUI/src/com/android/incallui/QtiCallUtils.java
@@ -255,6 +255,18 @@
     }
 
     /**
+     * Checks the boolean flag in config file to figure out if transmitting static image
+     * in a video call is enabled or not
+     */
+    public static boolean shallTransmitStaticImage(Context context) {
+        if (context == null) {
+            Log.w(context, "Context is null...");
+        }
+        return context != null && QtiImsExtUtils.shallTransmitStaticImage(context);
+    }
+
+
+    /**
      * Checks the boolean flag in config file to figure out if it support preview before the accept
      * video call or not
      */
diff --git a/InCallUI/src/com/android/incallui/VideoCallFragment.java b/InCallUI/src/com/android/incallui/VideoCallFragment.java
index 3a708ab..a210e6e 100644
--- a/InCallUI/src/com/android/incallui/VideoCallFragment.java
+++ b/InCallUI/src/com/android/incallui/VideoCallFragment.java
@@ -34,6 +34,9 @@
 import android.view.WindowManager;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
+import android.content.pm.PackageManager;
+import android.Manifest;
+import android.os.Process;
 
 import com.android.dialer.R;
 import com.android.phone.common.animation.AnimUtils;
@@ -66,6 +69,7 @@
      * Used to indicate that the UI rotation is unknown.
      */
     public static final int ORIENTATION_UNKNOWN = -1;
+    private static final int PERMISSION_REQUEST_READ_EXTERNAL_STORAGE = 1;
 
     // Static storage used to retain the video surfaces across Activity restart.
     // TextureViews are not parcelable, so it is not possible to store them in the saved state.
@@ -446,6 +450,46 @@
     }
 
     /**
+      * Callback received when a permissions request has been completed.
+      *
+      * @param requestCode The request code passed in requestPermissions API
+      * @param permissions The requested permissions. Never null.
+      * @param grantResults The grant results for the corresponding permissions
+      *     which is either PackageManager#PERMISSION_GRANTED}
+      *     or PackageManager#PERMISSION_DENIED}. Never null.
+      */
+    @Override
+    public void onRequestPermissionsResult(int requestCode, String[] permissions,
+            int[] grantResults) {
+        switch (requestCode) {
+            case PERMISSION_REQUEST_READ_EXTERNAL_STORAGE: {
+                // If request is cancelled, the result arrays are empty.
+                getPresenter().onReadStoragePermissionResponse(grantResults.length > 0 &&
+                        grantResults[0] == PackageManager.PERMISSION_GRANTED);
+                return;
+            }
+            default:
+                Log.w(TAG, "onRequestPermissionsResult: Unhandled requestCode = " + requestCode);
+        }
+    }
+
+    public void onRequestReadStoragePermission() {
+        final Activity activity = getActivity();
+        if (activity == null) {
+            Log.e(this, "onRequestReadStoragePermission: Activity is null");
+            return;
+        }
+        if ((activity.checkSelfPermission(
+                Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)) {
+            requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
+                    PERMISSION_REQUEST_READ_EXTERNAL_STORAGE);
+        } else {
+            // permission already granted
+            getPresenter().onReadStoragePermissionResponse(true);
+        }
+    }
+
+    /**
      * Handles creation of the activity and initialization of the presenter.
      *
      * @param savedInstanceState The saved instance state.
@@ -689,6 +733,18 @@
         return sPreviewSurface == null ? null : sPreviewSurface.getSurface();
     }
 
+    @Override
+    public Point getPreviewContainerSize() {
+        if (mPreviewVideoContainer == null) {
+            return null;
+        }
+        FrameLayout.LayoutParams params = (FrameLayout.LayoutParams)
+                mPreviewVideoContainer.getLayoutParams();
+        Log.d(this, "getPreviewContainerSize: width = " + params.width +
+                " height = " + params.height);
+        return new Point(params.width, params.height);
+    }
+
     /**
      * Changes the dimensions of the preview surface.  Called when the dimensions change due to a
      * device orientation change.
diff --git a/InCallUI/src/com/android/incallui/VideoCallPresenter.java b/InCallUI/src/com/android/incallui/VideoCallPresenter.java
index 58405a8..6a04de2 100644
--- a/InCallUI/src/com/android/incallui/VideoCallPresenter.java
+++ b/InCallUI/src/com/android/incallui/VideoCallPresenter.java
@@ -16,8 +16,13 @@
 
 package com.android.incallui;
 
+import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
 import android.graphics.Point;
 import android.net.Uri;
 import android.os.AsyncTask;
@@ -33,6 +38,7 @@
 import android.view.Surface;
 import android.widget.ImageView;
 
+import org.codeaurora.ims.QtiImsException;
 import org.codeaurora.ims.utils.QtiImsExtUtils;
 
 import com.android.contacts.common.ContactPhotoManager;
@@ -169,6 +175,10 @@
     private int mPreviewSurfaceState = PreviewSurfaceState.NONE;
 
     private static boolean mIsVideoMode = false;
+    // Holds TRUE if default image should be used as static image else holds FALSE
+    private static boolean sUseDefaultImage = false;
+    // Holds TRUE if static image needs to be transmitted instead of video preview stream
+    private static boolean sShallTransmitStaticImage = false;
 
     /**
      * Contact photo manager to retrieve cached contact photo information.
@@ -449,7 +459,10 @@
             case VideoCallFragment.SURFACE_PREVIEW:
                 if (mPictureModeHelper.canShowPreviewVideoView() &&
                         mPictureModeHelper.canShowIncomingVideoView()) {
-                    InCallZoomController.getInstance().onPreviewSurfaceClicked(mVideoCall);
+                    if (!shallTransmitStaticImage()) {
+                        // show zoom bar only when UE is showing video stream in preview
+                        InCallZoomController.getInstance().onPreviewSurfaceClicked(mVideoCall);
+                    }
                 } else {
                     isFullscreen = InCallPresenter.getInstance().toggleFullscreenMode();
                     Log.d(this, "toggleFullScreen = " + isFullscreen + "surfaceId =" + surfaceId);
@@ -490,6 +503,8 @@
             if (isVideoMode()) {
                 exitVideoMode();
             }
+            sShallTransmitStaticImage = false;
+            sUseDefaultImage = false;
             cleanupSurfaces();
         }
 
@@ -765,7 +780,8 @@
 
     private boolean isCameraRequired(int videoState) {
         return ((VideoProfile.isBidirectional(videoState) ||
-                VideoProfile.isTransmissionEnabled(videoState)) && !mIsInBackground);
+                VideoProfile.isTransmissionEnabled(videoState)) && !mIsInBackground &&
+                !shallTransmitStaticImage());
     }
 
     private boolean isCameraRequired() {
@@ -853,6 +869,175 @@
         mIsVideoMode = false;
     }
 
+    private boolean shallTransmitStaticImage() {
+        return sShallTransmitStaticImage;
+    }
+
+    private void setPreviewImage(Drawable previewDrawable) {
+        final VideoCallUi ui = getUi();
+        if (ui == null || previewDrawable == null) {
+            Log.e(this, "setPreviewImage: ui/previewDrawable is null");
+            return;
+        }
+
+        ImageView photoView = ui.getPreviewPhotoView();
+        if (photoView == null) {
+            Log.e(this, "setPreviewImage: previewView is null");
+            return;
+        }
+        photoView.setScaleType(ImageView.ScaleType.CENTER_CROP);
+        photoView.setImageDrawable(previewDrawable);
+    }
+
+    private void setPreviewImage(Bitmap previewBitmap) {
+        final VideoCallUi ui = getUi();
+        if (ui == null || previewBitmap == null) {
+            Log.e(this, "setPreviewImage: ui/previewBitmap is null");
+            return;
+        }
+
+        ImageView photoView = ui.getPreviewPhotoView();
+        if (photoView == null) {
+            Log.e(this, "setPreviewImage: previewView is null");
+            return;
+        }
+        photoView.setImageBitmap(previewBitmap);
+    }
+
+    private void setPauseImage() {
+        String uriStr = null;
+        Uri uri = null;
+
+        if (!QtiCallUtils.shallTransmitStaticImage(mContext) || mVideoCall == null) {
+            return;
+        }
+
+        if (shallTransmitStaticImage()) {
+            uriStr = sUseDefaultImage ? "" :
+                    QtiImsExtUtils.getStaticImageUriStr(mContext.getContentResolver());
+        }
+
+        uri = uriStr != null ? Uri.parse(uriStr) : null;
+        Log.d(this, "setPauseImage parsed uri = " + uri + " sUseDefaultImage = "
+                + sUseDefaultImage);
+        mVideoCall.setPauseImage(uri);
+    }
+
+    private Drawable getDefaultImage() {
+       return mContext.getResources().
+                    getDrawable(R.drawable.img_no_image_automirrored);
+    }
+
+    public void maybeLoadPreConfiguredImageAsync() {
+        Log.d(this, "maybeLoadPreConfiguredImageAsync: shallTransmitStaticImage = "
+                + shallTransmitStaticImage() + " sUseDefaultImage = " + sUseDefaultImage);
+
+        if (!shallTransmitStaticImage()) {
+            return;
+        }
+
+        if (sUseDefaultImage) {
+            /* no need to display toast message here since user would've been
+               already altered of default image usage */
+            setPreviewImage(getDefaultImage());
+            setPauseImage();
+            return;
+        }
+
+        final AsyncTask<Void, Void, Bitmap> task = new AsyncTask<Void, Void, Bitmap>() {
+            // Decode image in background.
+            @Override
+            protected Bitmap doInBackground(Void... params) {
+                try {
+                    return loadImage();
+                } catch (QtiImsException ex) {
+                    Log.e(this, "loadImage: ex = " + ex);
+                }
+                return null;
+            }
+
+            // Once complete, set bitmap/pause image.
+            @Override
+            protected void onPostExecute(Bitmap bitmap) {
+                sUseDefaultImage = bitmap == null;
+                if (sUseDefaultImage) {
+                    QtiCallUtils.displayToast(mContext, R.string.qti_ims_defaultImage_fallback);
+                    setPreviewImage(getDefaultImage());
+                } else {
+                    setPreviewImage(bitmap);
+                }
+                setPauseImage();
+            }
+        };
+        task.execute();
+    }
+
+    public void onReadStoragePermissionResponse(boolean isGranted) {
+        Log.d(this,"onReadStoragePermissionResponse: granted = " + isGranted);
+
+        // Use default image when permissions are not granted
+        sUseDefaultImage = !isGranted;
+        if (!isGranted) {
+            QtiCallUtils.displayToast(mContext, R.string.qti_ims_defaultImage_fallback);
+        }
+        showVideoUi(mCurrentVideoState, mCurrentCallState, isConfCall());
+    }
+
+    private Bitmap loadImage() throws QtiImsException {
+        final VideoCallUi ui = getUi();
+        if (ui == null) {
+            Log.w(this, "loadImage: ui is null");
+            return null;
+        }
+
+        Point previewDimensions = ui.getPreviewContainerSize();
+        if (previewDimensions == null) {
+            Log.w(this, "loadImage: previewDimensions is null");
+            return null;
+        }
+
+        Log.d(this, "loadImage: size: " + previewDimensions);
+        return QtiImsExtUtils.getStaticImage(mContext, previewDimensions.x,
+                previewDimensions.y);
+    }
+
+    /**
+     * Handles a change to the video call hide me selection
+     *
+     * @param shallTransmitStaticImage {@code true} if the app should show static image in preview,
+     * {@code false} otherwise.
+     */
+    @Override
+    public void onSendStaticImageStateChanged(boolean shallTransmitStaticImage) {
+
+        Log.d(this, "onSendStaticImageStateChanged: shallTransmitStaticImage: "
+                + shallTransmitStaticImage);
+
+        final VideoCallUi ui = getUi();
+        sShallTransmitStaticImage = shallTransmitStaticImage;
+
+        if (mPrimaryCall == null || !VideoUtils.isActiveVideoCall(mPrimaryCall)) {
+            Log.w(this, "onSendStaticImageStateChanged: received for non-active video call");
+            return;
+        }
+
+        if (mVideoCall == null || ui == null) {
+            Log.w(this, "onSendStaticImageStateChanged: VideoCall/VideoCallUi is null");
+            return;
+        }
+
+        enableCamera(mVideoCall, isCameraRequired(mCurrentVideoState));
+        if (shallTransmitStaticImage) {
+            // Handle showing static image in preview based on external storage permissions
+            ui.onRequestReadStoragePermission();
+        } else {
+            /* When not required to transmit static image, update video ui visibility
+               to reflect streaming video in preview */
+            showVideoUi(mCurrentVideoState, mCurrentCallState, isConfCall());
+            mVideoCall.setPauseImage(null);
+        }
+    }
+
     /**
      * Based on the current video state and call state, show or hide the incoming and
      * outgoing video surfaces.  The outgoing video surface is shown any time video is transmitting.
@@ -880,9 +1065,9 @@
                 mPictureModeHelper.canShowPreviewVideoView();
 
         Log.v(this, "showVideoUi : showIncoming = " + showIncomingVideo + " showOutgoing = "
-                + showOutgoingVideo);
+                + showOutgoingVideo + " shallTransmitStaticImage = " + shallTransmitStaticImage());
         if (showIncomingVideo || showOutgoingVideo) {
-            ui.showVideoViews(showOutgoingVideo, showIncomingVideo);
+            ui.showVideoViews(showOutgoingVideo && !shallTransmitStaticImage(), showIncomingVideo);
 
             boolean hidePreview = shallHidePreview(isConf, videoState);
             Log.v(this, "showVideoUi, hidePreview = " + hidePreview);
@@ -892,9 +1077,10 @@
 
             if (showOutgoingVideo) {
                 setPreviewSize(mDeviceOrientation, mPreviewAspectRatio);
+                maybeLoadPreConfiguredImageAsync();
             }
 
-            if (isVideoReceptionEnabled) {
+            if (isVideoReceptionEnabled && !shallTransmitStaticImage()) {
                 loadProfilePhotoAsync();
             }
         } else {
@@ -1056,6 +1242,10 @@
         mPreviewSurfaceState = PreviewSurfaceState.CAPABILITIES_RECEIVED;
         changePreviewDimensions(width, height);
 
+        if (shallTransmitStaticImage()) {
+            setPauseImage();
+        }
+
         // Check if the preview surface is ready yet; if it is, set it on the {@code VideoCall}.
         // If it not yet ready, it will be set when when creation completes.
         if (ui.isPreviewVideoSurfaceCreated()) {
@@ -1607,6 +1797,8 @@
         void adjustPreviewLocation(boolean shiftUp, int offset);
         void setPreviewRotation(int orientation);
         void showOutgoingVideoView(boolean show);
+        void onRequestReadStoragePermission();
+        Point getPreviewContainerSize();
     }
 
     /**