Merge "Tips Gesture Navigation Tutorial [Part 2]" into ub-launcher3-master
This commit is contained in:
commit
defb0562c4
|
@ -91,6 +91,17 @@
|
||||||
android:taskAffinity="${packageName}.locktask"
|
android:taskAffinity="${packageName}.locktask"
|
||||||
android:directBootAware="true" />
|
android:directBootAware="true" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="com.android.quickstep.interaction.BackGestureTutorialActivity"
|
||||||
|
android:autoRemoveFromRecents="true"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:screenOrientation="portrait">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.android.quickstep.action.BACK_GESTURE_TUTORIAL" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
<!--
|
|
||||||
Copyright (C) 2020 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.
|
|
||||||
-->
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="192dp"
|
|
||||||
android:height="192dp"
|
|
||||||
android:viewportWidth="192"
|
|
||||||
android:viewportHeight="192">
|
|
||||||
<path
|
|
||||||
android:pathData="M96,90.01"
|
|
||||||
android:strokeWidth="2.4297"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="#FFC800"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M153.24,48.09c-0.5,-1.51 -0.99,-2.86 -1.51,-4.12c-3.03,-7.1 -7.26,-13.45 -12.59,-18.88C127.68,13.42 112.4,7 96.1,7c-14,0 -27.66,4.93 -38.45,13.87C47,29.69 39.61,41.99 36.82,55.51c-0.76,3.9 -1.14,7.86 -1.14,11.78c0,16.39 6.36,31.77 17.9,43.3c2.65,2.65 5.59,5.08 8.74,7.23l0.09,24.14v22.03c0,5.33 4.82,10.01 10.32,10.01h0.41h3.85v0c0,6.23 4.53,10.93 10.53,10.93h16.94c6.01,0 10.54,-4.7 10.54,-10.93v0h4.31c5.56,0 10.26,-4.5 10.26,-9.83v-22.01c0.01,-0.06 0.01,-0.13 0.01,-0.2v-23.74c16.75,-11.15 26.73,-30.15 26.73,-50.94C156.31,60.76 155.28,54.3 153.24,48.09zM118.51,111.08l-0.46,0.29l-0.46,0.29v0.55v0.55v4.38v22.77l-14.12,0V95.9h14h2v-2v-8.5v-2h-2H74.53h-2v2v8.5v2h2h14v44.02l-14.13,0l-0.09,-23.21l-0.02,-4.3l0,-0.54l0,-0.54l-0.45,-0.29l-0.45,-0.29l-3.6,-2.36c-2.81,-1.84 -5.41,-3.95 -7.73,-6.28c-9.27,-9.26 -14.38,-21.63 -14.38,-34.82c0,-3.14 0.3,-6.31 0.9,-9.43C53.25,35.35 73.23,19 96.1,19c13.05,0 25.3,5.15 34.48,14.5c4.27,4.34 7.66,9.43 10.08,15.1c0.39,0.96 0.78,2.03 1.18,3.24c1.64,5 2.47,10.2 2.47,15.45c0,17.1 -8.27,32.58 -22.11,41.43L118.51,111.08z"
|
|
||||||
android:fillColor="#4285F4"/>
|
|
||||||
</vector>
|
|
|
@ -69,13 +69,6 @@
|
||||||
<!-- Content description for a close button. [CHAR LIMIT=NONE] -->
|
<!-- Content description for a close button. [CHAR LIMIT=NONE] -->
|
||||||
<string name="back_gesture_tutorial_close_button_content_description" translatable="false">Close</string>
|
<string name="back_gesture_tutorial_close_button_content_description" translatable="false">Close</string>
|
||||||
|
|
||||||
<!-- Title shown on the notification of Back gesture tutorial. [CHAR LIMIT=30] -->
|
|
||||||
<string name="back_gesture_tutorial_notification_title" translatable="false">Try the new back gesture</string>
|
|
||||||
<!-- Subtitle shown on the notification of Back gesture tutorial. [CHAR LIMIT=60] -->
|
|
||||||
<string name="back_gesture_tutorial_notification_subtitle" translatable="false">Learn how to go back while using your apps</string>
|
|
||||||
<!-- Action text shown on the notification of Back gesture tutorial. [CHAR LIMIT=14] -->
|
|
||||||
<string name="back_gesture_tutorial_notification_action_label" translatable="false">Try it</string>
|
|
||||||
|
|
||||||
<!-- Title shown during interactive part of Back gesture tutorial for right edge. [CHAR LIMIT=30] -->
|
<!-- Title shown during interactive part of Back gesture tutorial for right edge. [CHAR LIMIT=30] -->
|
||||||
<string name="back_gesture_tutorial_playground_title_swipe_inward_right_edge" translatable="false">Try the back gesture</string>
|
<string name="back_gesture_tutorial_playground_title_swipe_inward_right_edge" translatable="false">Try the back gesture</string>
|
||||||
<!-- Subtitle shown during interactive parts of Back gesture tutorial for right edge. [CHAR LIMIT=60] -->
|
<!-- Subtitle shown during interactive parts of Back gesture tutorial for right edge. [CHAR LIMIT=60] -->
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
<!--
|
|
||||||
Copyright (C) 2020 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.
|
|
||||||
-->
|
|
||||||
<alias xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<intent
|
|
||||||
android:action="com.android.quickstep.action.BACK_GESTURE_TUTORIAL" />
|
|
||||||
</alias>
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.quickstep.interaction;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.Window;
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
|
||||||
|
import com.android.launcher3.R;
|
||||||
|
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
|
||||||
|
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/** Shows the Back gesture interactive tutorial in full screen mode. */
|
||||||
|
public class BackGestureTutorialActivity extends FragmentActivity {
|
||||||
|
|
||||||
|
Optional<BackGestureTutorialFragment> mFragment = Optional.empty();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
setContentView(R.layout.back_gesture_tutorial_activity);
|
||||||
|
|
||||||
|
mFragment = Optional.of(BackGestureTutorialFragment.newInstance(TutorialStep.ENGAGED,
|
||||||
|
TutorialType.RIGHT_EDGE_BACK_NAVIGATION));
|
||||||
|
getSupportFragmentManager().beginTransaction()
|
||||||
|
.add(R.id.back_gesture_tutorial_fragment_container, mFragment.get())
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasFocus) {
|
||||||
|
super.onWindowFocusChanged(hasFocus);
|
||||||
|
if (hasFocus) {
|
||||||
|
hideSystemUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (mFragment.isPresent()) {
|
||||||
|
mFragment.get().onBackPressed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemUI() {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
getWindow().setNavigationBarColor(Color.TRANSPARENT);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.quickstep.interaction;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.android.launcher3.R;
|
||||||
|
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of {@link BackGestureTutorialController} that defines the behavior of the
|
||||||
|
* {@link TutorialStep#CONFIRM}.
|
||||||
|
*/
|
||||||
|
final class BackGestureTutorialConfirmController extends BackGestureTutorialController {
|
||||||
|
|
||||||
|
BackGestureTutorialConfirmController(BackGestureTutorialFragment fragment,
|
||||||
|
BackGestureTutorialTypeInfo tutorialTypeInfo) {
|
||||||
|
super(fragment, TutorialStep.CONFIRM, Optional.of(tutorialTypeInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Optional<Integer> getTitleStringId() {
|
||||||
|
return Optional.of(mTutorialTypeInfo.get().getTutorialConfirmTitleId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Optional<Integer> getSubtitleStringId() {
|
||||||
|
return Optional.of(mTutorialTypeInfo.get().getTutorialConfirmSubtitleId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Optional<Integer> getActionButtonStringId() {
|
||||||
|
return Optional.of(R.string.back_gesture_tutorial_action_button_label);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Optional<Integer> getActionTextButtonStringId() {
|
||||||
|
return Optional.of(R.string.back_gesture_tutorial_action_text_button_label);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void onActionButtonClicked(View button) {
|
||||||
|
hideHandCoachingAnimation();
|
||||||
|
if (button == mActionTextButton) {
|
||||||
|
mFragment.startSystemNavigationSetting();
|
||||||
|
}
|
||||||
|
mFragment.closeTutorial();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.quickstep.interaction;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.android.launcher3.R;
|
||||||
|
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
|
||||||
|
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the behavior of the particular {@link TutorialStep} and implements the transition to it.
|
||||||
|
*/
|
||||||
|
abstract class BackGestureTutorialController {
|
||||||
|
|
||||||
|
final BackGestureTutorialFragment mFragment;
|
||||||
|
final TutorialStep mTutorialStep;
|
||||||
|
final Optional<BackGestureTutorialTypeInfo> mTutorialTypeInfo;
|
||||||
|
final Button mActionTextButton;
|
||||||
|
final Button mActionButton;
|
||||||
|
final TextView mSubtitleTextView;
|
||||||
|
final ImageButton mCloseButton;
|
||||||
|
final BackGestureTutorialHandAnimation mHandCoachingAnimation;
|
||||||
|
final LinearLayout mTitlesContainer;
|
||||||
|
|
||||||
|
private final TextView mTitleTextView;
|
||||||
|
private final ImageView mHandCoachingView;
|
||||||
|
|
||||||
|
BackGestureTutorialController(
|
||||||
|
BackGestureTutorialFragment fragment,
|
||||||
|
TutorialStep tutorialStep,
|
||||||
|
Optional<BackGestureTutorialTypeInfo> tutorialTypeInfo) {
|
||||||
|
mFragment = fragment;
|
||||||
|
mTutorialStep = tutorialStep;
|
||||||
|
mTutorialTypeInfo = tutorialTypeInfo;
|
||||||
|
|
||||||
|
View rootView = fragment.getRootView();
|
||||||
|
mActionTextButton = rootView.findViewById(
|
||||||
|
R.id.back_gesture_tutorial_fragment_action_text_button);
|
||||||
|
mActionButton = rootView.findViewById(R.id.back_gesture_tutorial_fragment_action_button);
|
||||||
|
mSubtitleTextView = rootView.findViewById(
|
||||||
|
R.id.back_gesture_tutorial_fragment_subtitle_view);
|
||||||
|
mTitleTextView = rootView.findViewById(R.id.back_gesture_tutorial_fragment_title_view);
|
||||||
|
mHandCoachingView = rootView.findViewById(
|
||||||
|
R.id.back_gesture_tutorial_fragment_hand_coaching);
|
||||||
|
mHandCoachingAnimation = mFragment.getHandAnimation();
|
||||||
|
mHandCoachingView.bringToFront();
|
||||||
|
mCloseButton = rootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button);
|
||||||
|
mTitlesContainer = rootView.findViewById(
|
||||||
|
R.id.back_gesture_tutorial_fragment_titles_container);
|
||||||
|
}
|
||||||
|
|
||||||
|
void transitToController() {
|
||||||
|
updateTitles();
|
||||||
|
updateActionButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
void hideHandCoachingAnimation() {
|
||||||
|
mHandCoachingAnimation.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onGestureDetected() {
|
||||||
|
hideHandCoachingAnimation();
|
||||||
|
|
||||||
|
if (mTutorialStep == TutorialStep.CONFIRM) {
|
||||||
|
mFragment.closeTutorial();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mTutorialTypeInfo.get().getTutorialType() == TutorialType.RIGHT_EDGE_BACK_NAVIGATION) {
|
||||||
|
mFragment.changeController(TutorialStep.ENGAGED,
|
||||||
|
TutorialType.LEFT_EDGE_BACK_NAVIGATION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mFragment.changeController(TutorialStep.CONFIRM);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract Optional<Integer> getTitleStringId();
|
||||||
|
|
||||||
|
abstract Optional<Integer> getSubtitleStringId();
|
||||||
|
|
||||||
|
abstract Optional<Integer> getActionButtonStringId();
|
||||||
|
|
||||||
|
abstract Optional<Integer> getActionTextButtonStringId();
|
||||||
|
|
||||||
|
abstract void onActionButtonClicked(View button);
|
||||||
|
|
||||||
|
private void updateActionButtons() {
|
||||||
|
updateButton(mActionButton, getActionButtonStringId(), this::onActionButtonClicked);
|
||||||
|
updateButton(mActionTextButton, getActionTextButtonStringId(), this::onActionButtonClicked);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void updateButton(Button button, Optional<Integer> stringId,
|
||||||
|
View.OnClickListener listener) {
|
||||||
|
if (!stringId.isPresent()) {
|
||||||
|
button.setVisibility(View.INVISIBLE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.setVisibility(View.VISIBLE);
|
||||||
|
button.setText(stringId.get());
|
||||||
|
button.setOnClickListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTitles() {
|
||||||
|
updateTitleView(mTitleTextView, getTitleStringId(),
|
||||||
|
R.style.TextAppearance_BackGestureTutorial_Title);
|
||||||
|
updateTitleView(mSubtitleTextView, getSubtitleStringId(),
|
||||||
|
R.style.TextAppearance_BackGestureTutorial_Subtitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void updateTitleView(TextView textView, Optional<Integer> stringId,
|
||||||
|
int styleId) {
|
||||||
|
if (!stringId.isPresent()) {
|
||||||
|
textView.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
textView.setVisibility(View.VISIBLE);
|
||||||
|
textView.setText(stringId.get());
|
||||||
|
textView.setTextAppearance(styleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs {@link BackGestureTutorialController} for providing {@link TutorialType} and
|
||||||
|
* {@link TutorialStep}.
|
||||||
|
*/
|
||||||
|
static Optional<BackGestureTutorialController> getTutorialController(
|
||||||
|
BackGestureTutorialFragment fragment, TutorialStep tutorialStep,
|
||||||
|
TutorialType tutorialType) {
|
||||||
|
BackGestureTutorialTypeInfo tutorialTypeInfo =
|
||||||
|
BackGestureTutorialTypeInfoProvider.getTutorialTypeInfo(tutorialType);
|
||||||
|
switch (tutorialStep) {
|
||||||
|
case ENGAGED:
|
||||||
|
return Optional.of(
|
||||||
|
new BackGestureTutorialEngagedController(fragment, tutorialTypeInfo));
|
||||||
|
case CONFIRM:
|
||||||
|
return Optional.of(
|
||||||
|
new BackGestureTutorialConfirmController(fragment, tutorialTypeInfo));
|
||||||
|
default:
|
||||||
|
throw new AssertionError("Unexpected tutorial step: " + tutorialStep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.quickstep.interaction;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of {@link BackGestureTutorialController} that defines the behavior of the
|
||||||
|
* {@link TutorialStep#ENGAGED}.
|
||||||
|
*/
|
||||||
|
final class BackGestureTutorialEngagedController extends BackGestureTutorialController {
|
||||||
|
|
||||||
|
BackGestureTutorialEngagedController(
|
||||||
|
BackGestureTutorialFragment fragment, BackGestureTutorialTypeInfo tutorialTypeInfo) {
|
||||||
|
super(fragment, TutorialStep.ENGAGED, Optional.of(tutorialTypeInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void transitToController() {
|
||||||
|
super.transitToController();
|
||||||
|
mHandCoachingAnimation.maybeStartLoopedAnimation(mTutorialTypeInfo.get().getTutorialType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Optional<Integer> getTitleStringId() {
|
||||||
|
return Optional.of(mTutorialTypeInfo.get().getTutorialPlaygroundTitleId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Optional<Integer> getSubtitleStringId() {
|
||||||
|
return Optional.of(mTutorialTypeInfo.get().getTutorialEngagedSubtitleId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Optional<Integer> getActionButtonStringId() {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Optional<Integer> getActionTextButtonStringId() {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void onActionButtonClicked(View button) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.quickstep.interaction;
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
|
import com.android.launcher3.R;
|
||||||
|
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/** Shows the Back gesture interactive tutorial. */
|
||||||
|
public class BackGestureTutorialFragment extends Fragment {
|
||||||
|
|
||||||
|
private static final String LOG_TAG = "TutorialFragment";
|
||||||
|
private static final String KEY_TUTORIAL_STEP = "tutorialStep";
|
||||||
|
private static final String KEY_TUTORIAL_TYPE = "tutorialType";
|
||||||
|
private static final String SYSTEM_NAVIGATION_SETTING_INTENT =
|
||||||
|
"#Intent;action=com.android.settings.SEARCH_RESULT_TRAMPOLINE;S"
|
||||||
|
+ ".:settings:fragment_args_key=gesture_system_navigation_input_summary;S"
|
||||||
|
+ ".:settings:show_fragment=com.android.settings.gestures"
|
||||||
|
+ ".SystemNavigationGestureSettings;end";
|
||||||
|
|
||||||
|
private TutorialStep mTutorialStep;
|
||||||
|
private TutorialType mTutorialType;
|
||||||
|
private Optional<BackGestureTutorialController> mTutorialController = Optional.empty();
|
||||||
|
private View mRootView;
|
||||||
|
private BackGestureTutorialHandAnimation mHandCoachingAnimation;
|
||||||
|
|
||||||
|
public static BackGestureTutorialFragment newInstance(
|
||||||
|
TutorialStep tutorialStep, TutorialType tutorialType) {
|
||||||
|
BackGestureTutorialFragment fragment = new BackGestureTutorialFragment();
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putSerializable(KEY_TUTORIAL_STEP, tutorialStep);
|
||||||
|
args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType);
|
||||||
|
fragment.setArguments(args);
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
|
||||||
|
mTutorialStep = (TutorialStep) args.getSerializable(KEY_TUTORIAL_STEP);
|
||||||
|
mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(
|
||||||
|
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState);
|
||||||
|
|
||||||
|
mRootView = inflater.inflate(R.layout.back_gesture_tutorial_fragment,
|
||||||
|
container, /* attachToRoot= */ false);
|
||||||
|
mRootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button)
|
||||||
|
.setOnClickListener(this::onCloseButtonClicked);
|
||||||
|
mHandCoachingAnimation = new BackGestureTutorialHandAnimation(getContext(), mRootView);
|
||||||
|
|
||||||
|
return mRootView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
changeController(mTutorialStep, mTutorialType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
mHandCoachingAnimation.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(Bundle savedInstanceState) {
|
||||||
|
savedInstanceState.putSerializable(KEY_TUTORIAL_STEP, mTutorialStep);
|
||||||
|
savedInstanceState.putSerializable(KEY_TUTORIAL_TYPE, mTutorialType);
|
||||||
|
super.onSaveInstanceState(savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
View getRootView() {
|
||||||
|
return mRootView;
|
||||||
|
}
|
||||||
|
|
||||||
|
BackGestureTutorialHandAnimation getHandAnimation() {
|
||||||
|
return mHandCoachingAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
void changeController(TutorialStep tutorialStep) {
|
||||||
|
changeController(tutorialStep, mTutorialType);
|
||||||
|
}
|
||||||
|
|
||||||
|
void changeController(TutorialStep tutorialStep, TutorialType tutorialType) {
|
||||||
|
Optional<BackGestureTutorialController> tutorialController =
|
||||||
|
BackGestureTutorialController.getTutorialController(/* fragment= */ this,
|
||||||
|
tutorialStep, tutorialType);
|
||||||
|
if (!tutorialController.isPresent()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mTutorialController = tutorialController;
|
||||||
|
mTutorialController.get().transitToController();
|
||||||
|
this.mTutorialStep = mTutorialController.get().mTutorialStep;
|
||||||
|
this.mTutorialType = tutorialType;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onBackPressed() {
|
||||||
|
if (mTutorialController.isPresent()) {
|
||||||
|
mTutorialController.get().onGestureDetected();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void closeTutorial() {
|
||||||
|
getActivity().finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
void startSystemNavigationSetting() {
|
||||||
|
try {
|
||||||
|
startActivityForResult(
|
||||||
|
Intent.parseUri(SYSTEM_NAVIGATION_SETTING_INTENT, /* flags= */ 0),
|
||||||
|
/* requestCode= */ 0);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
Log.e(LOG_TAG, "The launch Intent Uri is wrong syntax: " + e);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
Log.e(LOG_TAG, "The launch Activity not found: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCloseButtonClicked(View button) {
|
||||||
|
closeTutorial();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Denotes the step of the tutorial. */
|
||||||
|
enum TutorialStep {
|
||||||
|
ENGAGED,
|
||||||
|
CONFIRM,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Denotes the type of the tutorial. */
|
||||||
|
enum TutorialType {
|
||||||
|
RIGHT_EDGE_BACK_NAVIGATION,
|
||||||
|
LEFT_EDGE_BACK_NAVIGATION,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.quickstep.interaction;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.Animatable2;
|
||||||
|
import android.graphics.drawable.AnimatedVectorDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.android.launcher3.R;
|
||||||
|
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/** Hand coaching animation. */
|
||||||
|
final class BackGestureTutorialHandAnimation {
|
||||||
|
|
||||||
|
// A delay for waiting the Activity fully launches.
|
||||||
|
private static final Duration ANIMATION_START_DELAY = Duration.ofMillis(300L);
|
||||||
|
|
||||||
|
private final ImageView mHandCoachingView;
|
||||||
|
private final AnimatedVectorDrawable mGestureAnimation;
|
||||||
|
|
||||||
|
private boolean mIsAnimationPlayed = false;
|
||||||
|
|
||||||
|
BackGestureTutorialHandAnimation(Context context, View rootView) {
|
||||||
|
mHandCoachingView = rootView.findViewById(
|
||||||
|
R.id.back_gesture_tutorial_fragment_hand_coaching);
|
||||||
|
mGestureAnimation = (AnimatedVectorDrawable) ContextCompat.getDrawable(context,
|
||||||
|
R.drawable.back_gesture);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isRunning() {
|
||||||
|
return mGestureAnimation.isRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts animation if the playground is launched for the first time.
|
||||||
|
*/
|
||||||
|
void maybeStartLoopedAnimation(TutorialType tutorialType) {
|
||||||
|
if (isRunning() || mIsAnimationPlayed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mIsAnimationPlayed = true;
|
||||||
|
clearAnimationCallbacks();
|
||||||
|
mGestureAnimation.registerAnimationCallback(
|
||||||
|
new Animatable2.AnimationCallback() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Drawable drawable) {
|
||||||
|
super.onAnimationEnd(drawable);
|
||||||
|
mGestureAnimation.start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
start(tutorialType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void start(TutorialType tutorialType) {
|
||||||
|
// Because the gesture animation has only the right side form.
|
||||||
|
// The left side form of the gesture animation is made from flipping the View.
|
||||||
|
float rotationY = tutorialType == TutorialType.LEFT_EDGE_BACK_NAVIGATION ? 180f : 0f;
|
||||||
|
mHandCoachingView.setRotationY(rotationY);
|
||||||
|
mHandCoachingView.setImageDrawable(mGestureAnimation);
|
||||||
|
mHandCoachingView.postDelayed(() -> mGestureAnimation.start(),
|
||||||
|
ANIMATION_START_DELAY.toMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearAnimationCallbacks() {
|
||||||
|
mGestureAnimation.clearAnimationCallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop() {
|
||||||
|
mIsAnimationPlayed = false;
|
||||||
|
clearAnimationCallbacks();
|
||||||
|
mGestureAnimation.stop();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.quickstep.interaction;
|
||||||
|
|
||||||
|
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
|
||||||
|
|
||||||
|
/** Defines the UI element identifiers for the particular {@link TutorialType}. */
|
||||||
|
final class BackGestureTutorialTypeInfo {
|
||||||
|
|
||||||
|
private final TutorialType mTutorialType;
|
||||||
|
private final int mTutorialPlaygroundTitleId;
|
||||||
|
private final int mTutorialEngagedSubtitleId;
|
||||||
|
private final int mTutorialConfirmTitleId;
|
||||||
|
private final int mTutorialConfirmSubtitleId;
|
||||||
|
|
||||||
|
TutorialType getTutorialType() {
|
||||||
|
return mTutorialType;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getTutorialPlaygroundTitleId() {
|
||||||
|
return mTutorialPlaygroundTitleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getTutorialEngagedSubtitleId() {
|
||||||
|
return mTutorialEngagedSubtitleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getTutorialConfirmTitleId() {
|
||||||
|
return mTutorialConfirmTitleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getTutorialConfirmSubtitleId() {
|
||||||
|
return mTutorialConfirmSubtitleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private BackGestureTutorialTypeInfo(
|
||||||
|
TutorialType tutorialType,
|
||||||
|
int tutorialPlaygroundTitleId,
|
||||||
|
int tutorialEngagedSubtitleId,
|
||||||
|
int tutorialConfirmTitleId,
|
||||||
|
int tutorialConfirmSubtitleId) {
|
||||||
|
mTutorialType = tutorialType;
|
||||||
|
mTutorialPlaygroundTitleId = tutorialPlaygroundTitleId;
|
||||||
|
mTutorialEngagedSubtitleId = tutorialEngagedSubtitleId;
|
||||||
|
mTutorialConfirmTitleId = tutorialConfirmTitleId;
|
||||||
|
mTutorialConfirmSubtitleId = tutorialConfirmSubtitleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builder for producing {@link BackGestureTutorialTypeInfo} objects. */
|
||||||
|
static class Builder {
|
||||||
|
|
||||||
|
private TutorialType mTutorialType;
|
||||||
|
private Integer mTutorialPlaygroundTitleId;
|
||||||
|
private Integer mTutorialEngagedSubtitleId;
|
||||||
|
private Integer mTutorialConfirmTitleId;
|
||||||
|
private Integer mTutorialConfirmSubtitleId;
|
||||||
|
|
||||||
|
Builder setTutorialType(TutorialType tutorialType) {
|
||||||
|
mTutorialType = tutorialType;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Builder setTutorialPlaygroundTitleId(int stringId) {
|
||||||
|
mTutorialPlaygroundTitleId = stringId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Builder setTutorialEngagedSubtitleId(int stringId) {
|
||||||
|
mTutorialEngagedSubtitleId = stringId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Builder setTutorialConfirmTitleId(int stringId) {
|
||||||
|
mTutorialConfirmTitleId = stringId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Builder setTutorialConfirmSubtitleId(int stringId) {
|
||||||
|
mTutorialConfirmSubtitleId = stringId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
BackGestureTutorialTypeInfo build() {
|
||||||
|
return new BackGestureTutorialTypeInfo(
|
||||||
|
mTutorialType,
|
||||||
|
mTutorialPlaygroundTitleId,
|
||||||
|
mTutorialEngagedSubtitleId,
|
||||||
|
mTutorialConfirmTitleId,
|
||||||
|
mTutorialConfirmSubtitleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2020 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.quickstep.interaction;
|
||||||
|
|
||||||
|
import com.android.launcher3.R;
|
||||||
|
import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType;
|
||||||
|
|
||||||
|
/** Provides instances of {@link BackGestureTutorialTypeInfo} for each {@link TutorialType}. */
|
||||||
|
final class BackGestureTutorialTypeInfoProvider {
|
||||||
|
|
||||||
|
private static final BackGestureTutorialTypeInfo RIGHT_EDGE_BACK_NAV_TUTORIAL_INFO =
|
||||||
|
BackGestureTutorialTypeInfo.builder()
|
||||||
|
.setTutorialType(TutorialType.RIGHT_EDGE_BACK_NAVIGATION)
|
||||||
|
.setTutorialPlaygroundTitleId(
|
||||||
|
R.string.back_gesture_tutorial_playground_title_swipe_inward_right_edge)
|
||||||
|
.setTutorialEngagedSubtitleId(
|
||||||
|
R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_right_edge)
|
||||||
|
.setTutorialConfirmTitleId(R.string.back_gesture_tutorial_confirm_title)
|
||||||
|
.setTutorialConfirmSubtitleId(R.string.back_gesture_tutorial_confirm_subtitle)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private static final BackGestureTutorialTypeInfo LEFT_EDGE_BACK_NAV_TUTORIAL_INFO =
|
||||||
|
BackGestureTutorialTypeInfo.builder()
|
||||||
|
.setTutorialType(TutorialType.LEFT_EDGE_BACK_NAVIGATION)
|
||||||
|
.setTutorialPlaygroundTitleId(
|
||||||
|
R.string.back_gesture_tutorial_playground_title_swipe_inward_left_edge)
|
||||||
|
.setTutorialEngagedSubtitleId(
|
||||||
|
R.string.back_gesture_tutorial_engaged_subtitle_swipe_inward_left_edge)
|
||||||
|
.setTutorialConfirmTitleId(R.string.back_gesture_tutorial_confirm_title)
|
||||||
|
.setTutorialConfirmSubtitleId(R.string.back_gesture_tutorial_confirm_subtitle)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
static BackGestureTutorialTypeInfo getTutorialTypeInfo(TutorialType tutorialType) {
|
||||||
|
switch (tutorialType) {
|
||||||
|
case RIGHT_EDGE_BACK_NAVIGATION:
|
||||||
|
return RIGHT_EDGE_BACK_NAV_TUTORIAL_INFO;
|
||||||
|
case LEFT_EDGE_BACK_NAVIGATION:
|
||||||
|
return LEFT_EDGE_BACK_NAV_TUTORIAL_INFO;
|
||||||
|
default:
|
||||||
|
throw new AssertionError("Unexpected tutorial type: " + tutorialType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BackGestureTutorialTypeInfoProvider() {
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue