1/*
2 * Copyright (c) 2022 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import Input from '@ohos.multimodalInput.inputEventClient';
17import touchEvent from '@ohos.multimodalInput.touchEvent';
18import {
19  Log,
20  windowManager,
21  CommonConstants
22} from '@ohos/common';
23
24const TAG = 'GestureNavigationExecutors';
25
26export default class GestureNavigationExecutors {
27  private static readonly HOME_DISTANCE_LIMIT_MIN = 0.1;
28  private static readonly RECENT_DISTANCE_LIMIT_MIN = 0.15;
29  private static readonly NS_PER_MS = 1000000;
30  private timeOfFirstLeavingTheBackEventHotArea: number | null = null;
31  private screenWidth = 0;
32  private screenHeight = 0;
33  private curEventType: touchEvent.Action | null = null;
34  private eventName: string | null = null;
35  private startEventPosition: {x: number, y: number} | null = null;
36  private preEventPosition: {x: number, y: number} | null = null;
37  private preEventTime: number | null = null;
38  private preSpeed = 0;
39  private startTime = 0;
40
41  private constructor() {
42  }
43
44  /**
45   * Set screenWidth.
46   */
47  setScreenWidth(screenWidth: number) {
48    this.screenWidth = screenWidth;
49  }
50
51  /**
52   * Set screenHeight.
53   */
54  setScreenHeight(screenHeight: number) {
55    this.screenHeight = screenHeight;
56  }
57
58  /**
59   * Get the GestureNavigationExecutors instance.
60   */
61  static getInstance(): GestureNavigationExecutors {
62    if (globalThis.sGestureNavigationExecutors == null) {
63      globalThis.sGestureNavigationExecutors = new GestureNavigationExecutors();
64    }
65    return globalThis.sGestureNavigationExecutors;
66  }
67
68  /**
69   * touchEvent Callback.
70   * @return true: Returns true if the gesture is within the specified hot zone.
71   */
72  touchEventCallback(event: touchEvent.TouchEvent): boolean {
73    Log.showDebug(TAG, `touchEventCallback enter. ${JSON.stringify(event)}`);
74    if (event.touches.length !== 1 || !event.actionTime || !event.action) {
75      return false;
76    }
77    const startXPosition = event.touches[0].screenX;
78    const startYPosition = event.touches[0].screenY;
79    if (event.action === touchEvent.Action.DOWN && this.isSpecifiesRegion(startXPosition, startYPosition)) {
80      this.initializationParameters();
81      this.startEventPosition = this.preEventPosition = {
82        x: startXPosition,
83        y: startYPosition
84      };
85      this.startTime = this.preEventTime = event.actionTime;
86      this.curEventType = event.action;
87      if (vp2px(16) >= startXPosition || startXPosition >= (this.screenWidth - vp2px(16))) {
88        this.eventName = 'backEvent';
89        return true;
90      }
91    }
92    if (this.startEventPosition && this.isSpecifiesRegion(this.startEventPosition.x, this.startEventPosition.y)) {
93      if (event.action === touchEvent.Action.MOVE) {
94        this.curEventType = event.action;
95        const curTime = event.actionTime;
96        const speedX = (startXPosition - this.preEventPosition.x) / ((curTime - this.preEventTime) / 1000);
97        const speedY = (startYPosition - this.preEventPosition.y) / ((curTime - this.preEventTime) / 1000);
98        const sqrt = Math.sqrt(speedX * speedX + speedY * speedY);
99        const curSpeed = startYPosition <= this.preEventPosition.y ? -sqrt : sqrt;
100        const acceleration = (curSpeed - this.preSpeed) / ((curTime - this.preEventTime) / 1000);
101        this.preEventPosition = {
102          x: startXPosition,
103          y: startYPosition
104        };
105        this.preSpeed = curSpeed;
106        const isDistance = this.isRecentsViewShowOfDistanceLimit(startYPosition);
107        const isSpeed = this.isRecentsViewShowOfSpeedLimit(curTime, acceleration, curSpeed);
108        this.preEventTime = curTime;
109        if (isDistance && isSpeed && !this.eventName && curSpeed) {
110          this.eventName = 'recentEvent';
111          this.recentEventCall();
112          return true;
113        }
114        if (this.eventName == 'backEvent' && startXPosition > vp2px(16) && !this.timeOfFirstLeavingTheBackEventHotArea) {
115          this.timeOfFirstLeavingTheBackEventHotArea = (curTime - this.startTime) / 1000;
116        }
117      }
118      if (event.action === touchEvent.Action.UP) {
119        let distance = 0;
120        let slidingSpeed = 0;
121        if (this.curEventType === touchEvent.Action.MOVE) {
122          if (this.eventName == 'backEvent') {
123            distance = Math.abs((startXPosition - this.startEventPosition.x));
124            if (distance >= vp2px(16) * 1.2 && this.timeOfFirstLeavingTheBackEventHotArea <= 120) {
125              this.backEventCall();
126              this.initializationParameters();
127              return true;
128            }
129          } else if (this.eventName == 'recentEvent') {
130            this.initializationParameters();
131            return true;
132          } else {
133            distance = this.startEventPosition.y - startYPosition;
134            const isDistance = this.isHomeViewShowOfDistanceLimit(startYPosition);
135            Log.showDebug(TAG, `touchEventCallback isDistance: ${isDistance}`);
136            if (isDistance) {
137              slidingSpeed = distance / ((event.actionTime - this.startTime) / GestureNavigationExecutors.NS_PER_MS);
138              Log.showDebug(TAG, `touchEventCallback homeEvent slidingSpeed: ${slidingSpeed}`);
139              if (slidingSpeed >= vp2px(500)) {
140                this.homeEventCall();
141              }
142              this.initializationParameters();
143              return true;
144            }
145          }
146        }
147        this.initializationParameters();
148      }
149    }
150    return false;
151  }
152
153  private initializationParameters() {
154    this.startEventPosition = null;
155    this.eventName = null;
156    this.preEventPosition = null;
157    this.timeOfFirstLeavingTheBackEventHotArea = null;
158    this.startTime = 0;
159    this.preSpeed = 0;
160  }
161
162  private backEventCall() {
163    Log.showInfo(TAG, 'backEventCall backEvent start');
164    let keyEvent = {
165      isPressed: true,
166      keyCode: 2,
167      keyDownDuration: 1,
168      isIntercepted: false
169    };
170
171    let res = Input.injectEvent({KeyEvent: keyEvent});
172    Log.showDebug(TAG, `backEventCall result: ${res}`);
173    keyEvent = {
174      isPressed: false,
175      keyCode: 2,
176      keyDownDuration: 1,
177      isIntercepted: false
178    };
179
180    setTimeout(() => {
181      res = Input.injectEvent({KeyEvent: keyEvent});
182      Log.showDebug(TAG, `backEventCall result: ${res}`);
183    }, 20)
184  }
185
186  private homeEventCall() {
187    Log.showInfo(TAG, 'homeEventCall homeEvent start');
188    globalThis.desktopContext.startAbility({
189      bundleName: CommonConstants.LAUNCHER_BUNDLE,
190      abilityName: CommonConstants.LAUNCHER_ABILITY
191    })
192      .then(() => {
193        Log.showDebug(TAG, 'homeEventCall startAbility Promise in service successful.');
194      })
195      .catch(() => {
196        Log.showDebug(TAG, 'homeEventCall startAbility Promise in service failed.');
197      });
198  }
199
200  private recentEventCall() {
201    Log.showInfo(TAG, 'recentEventCall recentEvent start');
202    windowManager.minimizeAllApps();
203    windowManager.createWindowWithName(windowManager.RECENT_WINDOW_NAME, windowManager.RECENT_RANK);
204  }
205
206  private isRecentsViewShowOfDistanceLimit(eventY: number) {
207    return (this.screenHeight - eventY) / this.screenHeight >= GestureNavigationExecutors.RECENT_DISTANCE_LIMIT_MIN;
208  }
209
210  private isHomeViewShowOfDistanceLimit(eventY: number) {
211    return (this.screenHeight - eventY) / this.screenHeight >= GestureNavigationExecutors.HOME_DISTANCE_LIMIT_MIN;
212  }
213
214  private isRecentsViewShowOfSpeedLimit(curTime: number, acceleration: number, curSpeed: number): boolean {
215    const MIN_ACCELERATION = 0.05;
216    const CUR_SPEED_NEGATIVE_FIVE = -5.0;
217    const CUR_SPEED_NEGATIVE_ONE = -1.0;
218    const TIMESTAMP_CONVERTED_TO_MILLISECONDS_CARDINALITY = 1000;// 时间戳转换为毫秒的基数
219    const MIN_TIME_DIFFERENCE = 10.0;// 最小时间差
220    return (acceleration > MIN_ACCELERATION && curSpeed > CUR_SPEED_NEGATIVE_FIVE) ||
221      ((curSpeed > CUR_SPEED_NEGATIVE_ONE) &&
222        (curTime - this.preEventTime) / TIMESTAMP_CONVERTED_TO_MILLISECONDS_CARDINALITY > MIN_TIME_DIFFERENCE);
223  }
224
225  private isSpecifiesRegion(startXPosition: number, startYPosition: number) {
226    const isStatusBarRegion = startYPosition <= this.screenHeight * 0.07;
227    const isSpecifiesXRegion = startXPosition <= vp2px(16) || startXPosition >= (this.screenWidth - vp2px(16));
228    const isSpecifiesYRegion = (this.screenHeight - vp2px(22)) <= startYPosition && startYPosition <= this.screenHeight;
229    return (isSpecifiesXRegion && !isStatusBarRegion) || (isSpecifiesYRegion && !isSpecifiesXRegion);
230  }
231}