blob: 6b19557159d3a490fd674d9eebc0e4ddbaee720d [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.car.privacy;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.constraintlayout.motion.widget.MotionLayout;
import com.android.systemui.R;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Car optimized Mic Privacy Chip View that is shown when microphone is being used.
*
* State flows:
* Base state:
* <ul>
* <li>INVISIBLE - Start Mic Use ->> Mic Status?</li>
* </ul>
* Mic On:
* <ul>
* <li>Mic Status? - On ->> ACTIVE_INIT</li>
* <li>ACTIVE_INIT - delay ->> ACTIVE</li>
* <li>ACTIVE - Stop Mic Use ->> INACTIVE</li>
* <li>INACTIVE - delay ->> INVISIBLE</li>
* </ul>
* Mic Off:
* <ul>
* <li>Mic Status? - Off ->> MICROPHONE_OFF</li>
* <li>MICROPHONE_OFF - delay ->> INVISIBLE</li>
* </ul>
*/
public class MicPrivacyChip extends MotionLayout {
private final static boolean DEBUG = false;
private final static String TAG = "MicPrivacyChip";
private final static String TYPES_TEXT_MICROPHONE = "microphone";
private final int mDelayPillToCircle;
private final int mDelayToNoMicUsage;
private AnimationStates mCurrentTransitionState;
private boolean mIsInflated;
private ScheduledExecutorService mExecutor;
private enum AnimationStates {
INVISIBLE,
ACTIVE_INIT,
ACTIVE,
INACTIVE,
MICROPHONE_OFF,
}
public MicPrivacyChip(@NonNull Context context) {
this(context, /* attrs= */ null);
}
public MicPrivacyChip(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, /* defStyleAttrs= */ 0);
}
public MicPrivacyChip(@NonNull Context context,
@Nullable AttributeSet attrs, int defStyleAttrs) {
super(context, attrs, defStyleAttrs);
mDelayPillToCircle = getResources().getInteger(R.integer.privacy_chip_pill_to_circle_delay);
mDelayToNoMicUsage = getResources().getInteger(R.integer.privacy_chip_no_mic_usage_delay);
mExecutor = Executors.newSingleThreadScheduledExecutor();
mIsInflated = false;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mCurrentTransitionState = AnimationStates.INVISIBLE;
mIsInflated = true;
}
private boolean isMicrophoneToggledOff() {
// TODO(182826082): Implement Microphone off functionality
return false;
}
private void setContentDescription(boolean isMicOff) {
if (isMicOff) {
// TODO(182826082): Implement Microphone off functionality
setContentDescription(null);
} else {
setContentDescription(
getResources().getString(R.string.ongoing_privacy_chip_content_multiple_apps,
TYPES_TEXT_MICROPHONE));
}
}
/**
* Starts reveal animation for Mic Privacy Chip.
*/
@UiThread
public void animateIn() {
if (!mIsInflated) {
if (DEBUG) Log.d(TAG, "Layout not inflated");
return;
}
if (mCurrentTransitionState == null) {
if (DEBUG) Log.d(TAG, "Current transition state is null or empty.");
return;
}
if (mCurrentTransitionState.equals(AnimationStates.INVISIBLE)) {
if (DEBUG) Log.d(TAG, isMicrophoneToggledOff() ? "setTransition: micOffFromInvisible"
: "setTransition: activeInitFromInvisible");
setTransition(isMicrophoneToggledOff() ? R.id.micOffFromInvisible
: R.id.activeInitFromInvisible);
} else if (mCurrentTransitionState.equals(AnimationStates.INACTIVE)) {
if (DEBUG) Log.d(TAG, isMicrophoneToggledOff() ? "setTransition: micOffFromInactive"
: "setTransition: activeInitFromInactive");
setTransition(isMicrophoneToggledOff() ? R.id.micOffFromInactive
: R.id.activeInitFromInactive);
} else {
if (DEBUG) Log.d(TAG, "Early exit, mCurrentTransitionState= "
+ mCurrentTransitionState);
return;
}
mExecutor.shutdownNow();
mExecutor = Executors.newSingleThreadScheduledExecutor();
// TODO(182938429): Use Transition Listeners once ConstraintLayout 2.0.0 is being used.
setContentDescription(false);
setVisibility(View.VISIBLE);
transitionToEnd();
mCurrentTransitionState = AnimationStates.ACTIVE_INIT;
if (!isMicrophoneToggledOff()) {
mExecutor.schedule(MicPrivacyChip.this::animateToOrangeCircle, mDelayPillToCircle,
TimeUnit.MILLISECONDS);
}
}
// TODO(182938429): Use Transition Listeners once ConstraintLayout 2.0.0 is being used.
private void animateToOrangeCircle() {
setTransition(R.id.activeFromActiveInit);
// Since this is launched using a {@link ScheduledExecutorService}, its UI based elements
// need to execute on main executor.
getContext().getMainExecutor().execute(() -> {
mCurrentTransitionState = AnimationStates.ACTIVE;
transitionToEnd();
});
}
/**
* Starts conceal animation for Mic Privacy Chip.
*/
@UiThread
public void animateOut() {
if (!mIsInflated) {
if (DEBUG) Log.d(TAG, "Layout not inflated");
return;
}
if (mCurrentTransitionState.equals(AnimationStates.ACTIVE_INIT)) {
if (DEBUG) Log.d(TAG, "setTransition: inactiveFromActiveInit");
setTransition(R.id.inactiveFromActiveInit);
} else if (mCurrentTransitionState.equals(AnimationStates.ACTIVE)) {
if (DEBUG) Log.d(TAG, "setTransition: inactiveFromActive");
setTransition(R.id.inactiveFromActive);
} else {
if (DEBUG) Log.d(TAG, "Early exit, mCurrentTransitionState= "
+ mCurrentTransitionState);
return;
}
mExecutor.shutdownNow();
mExecutor = Executors.newSingleThreadScheduledExecutor();
if (mCurrentTransitionState.equals(AnimationStates.MICROPHONE_OFF)) {
mCurrentTransitionState = AnimationStates.INACTIVE;
mExecutor.schedule(MicPrivacyChip.this::reset, mDelayToNoMicUsage,
TimeUnit.MILLISECONDS);
return;
}
// TODO(182938429): Use Transition Listeners once ConstraintLayout 2.0.0 is being used.
mCurrentTransitionState = AnimationStates.INACTIVE;
transitionToEnd();
mExecutor.schedule(MicPrivacyChip.this::reset, mDelayToNoMicUsage,
TimeUnit.MILLISECONDS);
}
// TODO(182938429): Use Transition Listeners once ConstraintLayout 2.0.0 is being used.
private void reset() {
if (isMicrophoneToggledOff()) {
if (DEBUG) Log.d(TAG, "setTransition: invisibleFromMicOff");
setTransition(R.id.invisibleFromMicOff);
} else {
if (DEBUG) Log.d(TAG, "setTransition: invisibleFromInactive");
setTransition(R.id.invisibleFromInactive);
}
// Since this is launched using a {@link ScheduledExecutorService}, its UI based elements
// need to execute on main executor.
getContext().getMainExecutor().execute(() -> {
mCurrentTransitionState = AnimationStates.INVISIBLE;
transitionToEnd();
setVisibility(View.GONE);
});
}
}