This is an old revision of the document!

/* * Headwind Remote: Open Source Remote Access Software for Android * https://headwind-remote.com * * Copyright © 2022 headwind-remote.com * * 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.hmdm.control;

import android.app.AlertDialog; import android.app.Dialog; import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.graphics.PixelFormat; import android.media.projection.MediaProjectionManager; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.util.DisplayMetrics; import android.util.Log; import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.WindowManager; import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import com.hmdm.control.janus.SharingEngineJanus;

public class MainActivity extends AppCompatActivity implements SharingEngineJanus.EventListener, SharingEngineJanus.StateListener {

  private ImageView imageViewConnStatus;
  private TextView textViewConnStatus;
  private EditText editTextSessionId;
  private EditText editTextPassword;
  private TextView textViewComment;
  private TextView textViewConnect;
  private TextView textViewSendLink;
  private TextView textViewExit;
  private ImageView overlayDot;
  private Handler handler = new Handler();
  private int overlayDotAlpha;
  private int overlayDotDirection = 1;
  private Dialog exitOnIdleDialog;
  private int exitCounter;
  private static final int EXIT_PROMPT_SEC = 10;
  private static final int OVERLAY_DOT_ANIMATION_INCREMENT = 20;
  private static final int OVERLAY_DOT_ANIMATION_DELAY = 200;
  private SharingEngine sharingEngine;
  private SettingsHelper settingsHelper;
  private String sessionId;
  private String password;
  private String adminName;
  private final static String ATTR_SESSION_ID = "sessionId";
  private final static String ATTR_PASSWORD = "password";
  private final static String ATTR_ADMIN_NAME = "adminName";
  private boolean needReconnect = false;
  private MediaProjectionManager projectionManager;
  private BroadcastReceiver mSharingServiceReceiver = new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
          if (intent == null || intent.getAction() == null) {
              return;
          }
          if (intent.getAction().equals(Const.ACTION_SCREEN_SHARING_START)) {
              notifySharingStart();
          } else if (intent.getAction().equals(Const.ACTION_SCREEN_SHARING_STOP)) {
              notifySharingStop();
              adminName = null;
              updateUI();
              cancelSharingTimeout();
              scheduleExitOnIdle();
          } else if (intent.getAction().equals(Const.ACTION_SCREEN_SHARING_FAILED)) {
              String message = intent.getStringExtra(Const.EXTRA_MESSAGE);
              if (message != null) {
                  Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show();
              }
              adminName = null;
              updateUI();
              cancelSharingTimeout();
              scheduleExitOnIdle();
          } else if (intent.getAction().equals(Const.ACTION_CONNECTION_FAILURE)) {
              sharingEngine.setState(Const.STATE_DISCONNECTED);
              Toast.makeText(MainActivity.this, R.string.connection_failure_hint, Toast.LENGTH_LONG).show();
              updateUI();
          } else if (intent.getAction().equals(Const.ACTION_SCREEN_SHARING_PERMISSION_NEEDED)) {
              startActivityForResult(projectionManager.createScreenCaptureIntent(), Const.REQUEST_SCREEN_SHARE);
          }
      }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      if (savedInstanceState != null) {
          restoreInstanceState(savedInstanceState);
      }
      Toolbar toolbar = findViewById(R.id.toolbar);
      setSupportActionBar(toolbar);
      settingsHelper = SettingsHelper.getInstance(this);
      sharingEngine = SharingEngineFactory.getSharingEngine();
      sharingEngine.setEventListener(this);
      sharingEngine.setStateListener(this);
      DisplayMetrics metrics = new DisplayMetrics();
      ScreenSharingHelper.getRealScreenSize(this, metrics);
      float videoScale = ScreenSharingHelper.adjustScreenMetrics(metrics);
      settingsHelper.setFloat(SettingsHelper.KEY_VIDEO_SCALE, videoScale);
      ScreenSharingHelper.setScreenMetrics(this, metrics.widthPixels, metrics.heightPixels, metrics.densityDpi);
      sharingEngine.setScreenWidth(metrics.widthPixels);
      sharingEngine.setScreenHeight(metrics.heightPixels);
      IntentFilter intentFilter = new IntentFilter(Const.ACTION_SCREEN_SHARING_START);
      intentFilter.addAction(Const.ACTION_SCREEN_SHARING_STOP);
      intentFilter.addAction(Const.ACTION_SCREEN_SHARING_PERMISSION_NEEDED);
      intentFilter.addAction(Const.ACTION_SCREEN_SHARING_FAILED);
      intentFilter.addAction(Const.ACTION_CONNECTION_FAILURE);
      LocalBroadcastManager.getInstance(this).registerReceiver(mSharingServiceReceiver, intentFilter);
      projectionManager = (MediaProjectionManager)getSystemService(Context.MEDIA_PROJECTION_SERVICE);
      initUI();
      setDefaultSettings();
  }
  @Override
  protected void onResume() {
      super.onResume();
      updateUI();
      startService(new Intent(MainActivity.this, GestureDispatchService.class));

connect();

      checkAccessibility();
  }
  private void checkAccessibility() {
      if (!Utils.isAccessibilityPermissionGranted(this)) {
          textViewConnect.setVisibility(View.INVISIBLE);
          new AlertDialog.Builder(this)
                  .setMessage(R.string.accessibility_hint)
                  .setPositiveButton(R.string.continue_button, new DialogInterface.OnClickListener() {
                      @Override
                      public void onClick(DialogInterface dialog, int which) {
                          Intent intent = new Intent(android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS);
                          try {
                              startActivityForResult(intent, 0);
                          } catch (Exception e) {
                              // Accessibility settings cannot be opened
                              reportAccessibilityUnavailable();
                          }
                      }
                  })
                  .setCancelable(false)
                  .create()
                  .show();
      } else {
          configureAndConnect();
      }
  }
  private void reportAccessibilityUnavailable() {
      new AlertDialog.Builder(this)
              .setMessage(R.string.accessibility_unavailable_error)
              .setPositiveButton(R.string.button_exit, new DialogInterface.OnClickListener() {
                  @Override
                  public void onClick(DialogInterface dialog, int which) {
                      exitApp();
                  }
              })
              .setCancelable(false)
              .create()
              .show();
  }
  private void configureAndConnect() {
      if (settingsHelper.getString(SettingsHelper.KEY_SERVER_URL) == null) {
          // Not configured yet
          settingsHelper.setString(SettingsHelper.KEY_SERVER_URL, BuildConfig.DEFAULT_SERVER_URL);
          settingsHelper.setString(SettingsHelper.KEY_SECRET, BuildConfig.DEFAULT_SECRET);
          settingsHelper.setBoolean(SettingsHelper.KEY_USE_DEFAULT, !BuildConfig.DEFAULT_SECRET.equals(""));
          Intent intent = new Intent(this, SettingsActivity.class);
          startActivityForResult(intent, Const.REQUEST_SETTINGS);
          return;
      }
      if (needReconnect) {
          // Here we go after changing settings
          needReconnect = false;
          if (sharingEngine.getState() != Const.STATE_DISCONNECTED) {
              sharingEngine.disconnect(MainActivity.this, (success, errorReason) -> connect());
          } else {
              connect();
          }
      } else {
          if (sharingEngine.getState() == Const.STATE_DISCONNECTED && sharingEngine.getErrorReason() == null) {
              connect();
          }
      }
  }
  @Override
  public void onDestroy() {
      try {
          LocalBroadcastManager.getInstance(this).unregisterReceiver(mSharingServiceReceiver);
      } catch (Exception e) {
      }
      super.onDestroy();
  }
  @Override
  public void onSaveInstanceState(Bundle savedInstanceState) {
      savedInstanceState.putString(ATTR_SESSION_ID, sessionId);
      savedInstanceState.putString(ATTR_PASSWORD, password);
      savedInstanceState.putString(ATTR_ADMIN_NAME, adminName);
      super.onSaveInstanceState(savedInstanceState);
  }
  private void restoreInstanceState(Bundle savedInstanceState) {
      sessionId = savedInstanceState.getString(ATTR_SESSION_ID);
      password = savedInstanceState.getString(ATTR_PASSWORD);
      adminName = savedInstanceState.getString(ATTR_ADMIN_NAME);
  }
  @Override
  public void onBackPressed() {
      Toast.makeText(this, R.string.back_pressed, Toast.LENGTH_LONG).show();
  }
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
      // Inflate the menu; this adds items to the action bar if it is present.
      getMenuInflater().inflate(R.menu.menu_main, menu);
      return true;
  }
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
      // Handle action bar item clicks here. The action bar will
      // automatically handle clicks on the Home/Up button, so long
      // as you specify a parent activity in AndroidManifest.xml.
      int id = item.getItemId();
      if (id == R.id.action_settings) {
          if (adminName != null) {
              Toast.makeText(this, R.string.settings_unavailable, Toast.LENGTH_LONG).show();
              return true;
          }
          Intent intent = new Intent(this, SettingsActivity.class);
          startActivityForResult(intent, Const.REQUEST_SETTINGS);
          cancelExitOnIdle();
          return true;
      } else if (id == R.id.action_about) {
          showAbout();
          return true;
      }
      return super.onOptionsItemSelected(item);
  }
  @Override
  public void onActivityResult(int requestCode, int resultCode, Intent data) {
      super.onActivityResult(requestCode, resultCode, data);
      if (requestCode == Const.REQUEST_SETTINGS) {
          if (resultCode == Const.RESULT_DIRTY) {
              needReconnect = true;
          } else {
              scheduleExitOnIdle();
          }
      } else if (requestCode == Const.REQUEST_SCREEN_SHARE) {
          if (resultCode != RESULT_OK) {
              Toast.makeText(this, R.string.screen_cast_denied, Toast.LENGTH_LONG).show();
              adminName = null;
              updateUI();
              cancelSharingTimeout();
              scheduleExitOnIdle();
          } else {
              ScreenSharingHelper.startSharing(this, resultCode, data);
          }
      }
  }
  private void initUI() {
      imageViewConnStatus = findViewById(R.id.image_conn_status);
      textViewConnStatus = findViewById(R.id.conn_status);
      editTextSessionId = findViewById(R.id.session_id_edit);
      editTextPassword = findViewById(R.id.password_edit);
      textViewComment = findViewById(R.id.comment);
      textViewConnect = findViewById(R.id.reconnect);
      textViewSendLink = findViewById(R.id.send_link);
      textViewExit = findViewById(R.id.disconnect_exit);
      textViewConnect.setOnClickListener(v -> connect());
      textViewSendLink.setOnClickListener(v -> sendLink());
      textViewExit.setOnClickListener(v -> gracefulExit());
  }
  private void gracefulExit() {
      if (adminName != null) {
          notifySharingStop();
          ScreenSharingHelper.stopSharing(MainActivity.this, true);
      }
      sharingEngine.disconnect(MainActivity.this, (success, errorReason) -> exitApp());
      // 5 sec timeout to exit
      handler.postDelayed(() -> exitApp(), 5000);
  }
  private void exitApp() {
      Intent intent = new Intent(MainActivity.this, ScreenSharingService.class);
      stopService(intent);
      intent = new Intent(MainActivity.this, GestureDispatchService.class);
      stopService(intent);
      finishAffinity();
      System.exit(0);
  }
  private void updateUI() {
      int[] stateLabels = {R.string.state_disconnected, R.string.state_connecting, R.string.state_connected, R.string.state_sharing, R.string.state_disconnecting};
      int[] stateImages = {R.drawable.ic_disconnected, R.drawable.ic_connecting, R.drawable.ic_connected, R.drawable.ic_sharing, R.drawable.ic_connecting};
      int state = sharingEngine.getState();
      if (state == Const.STATE_CONNECTED && adminName != null) {
          imageViewConnStatus.setImageDrawable(getDrawable(stateImages[Const.STATE_SHARING]));
          textViewConnStatus.setText(stateLabels[Const.STATE_SHARING]);
      } else {
          imageViewConnStatus.setImageDrawable(getDrawable(stateImages[state]));
          textViewConnStatus.setText(stateLabels[state]);
      }
      String serverUrl = Utils.prepareDisplayUrl(settingsHelper.getString(SettingsHelper.KEY_SERVER_URL));
      textViewSendLink.setVisibility(state == Const.STATE_CONNECTED ? View.VISIBLE : View.INVISIBLE);
      textViewConnect.setVisibility(state == Const.STATE_DISCONNECTED ? View.VISIBLE : View.INVISIBLE);
      switch (state) {
          case Const.STATE_DISCONNECTED:
              editTextSessionId.setText("");
              editTextPassword.setText("");
              if (sharingEngine.getErrorReason() != null) {
                  textViewComment.setText(getString(R.string.hint_connection_error, serverUrl));
              }
              break;
          case Const.STATE_CONNECTING:
              textViewComment.setText(getString(R.string.hint_connecting, serverUrl));
              break;
          case Const.STATE_DISCONNECTING:
              textViewComment.setText(getString(R.string.hint_disconnecting));
              break;
          case Const.STATE_CONNECTED:
              editTextSessionId.setText(sessionId);
              editTextPassword.setText(password);
              textViewComment.setText(adminName != null ?
                      getString(R.string.hint_sharing, adminName) :
                      getString(R.string.hint_connected, serverUrl)
                      );
              break;
      }
  }
  private void setDefaultSettings() {
      if (settingsHelper.getString(SettingsHelper.KEY_DEVICE_NAME) == null) {
          settingsHelper.setString(SettingsHelper.KEY_DEVICE_NAME, Build.MANUFACTURER + " " + Build.MODEL);
      }
      if (settingsHelper.getInt(SettingsHelper.KEY_BITRATE) == 0) {
          settingsHelper.setInt(SettingsHelper.KEY_BITRATE, Const.DEFAULT_BITRATE);
      }
      if (settingsHelper.getInt(SettingsHelper.KEY_FRAME_RATE) == 0) {
          settingsHelper.setInt(SettingsHelper.KEY_FRAME_RATE, Const.DEFAULT_FRAME_RATE);
      }
      if (settingsHelper.getInt(SettingsHelper.KEY_IDLE_TIMEOUT) == 0) {
          settingsHelper.setInt(SettingsHelper.KEY_IDLE_TIMEOUT, Const.DEFAULT_IDLE_TIMEOUT);
      }
      if (settingsHelper.getInt(SettingsHelper.KEY_PING_TIMEOUT) == 0) {
          settingsHelper.setInt(SettingsHelper.KEY_PING_TIMEOUT, Const.DEFAULT_PING_TIMEOUT);
      }
  }
  private void sendLink() {
      String url = settingsHelper.getString(SettingsHelper.KEY_SERVER_URL);
      url += "?session=" + sessionId + "&pin=" + password;
      try {
          Intent shareIntent = new Intent(Intent.ACTION_SEND);
          shareIntent.setType("text/plain");
          shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.send_link_subject));
          String shareMessage= getString(R.string.send_link_message, url, settingsHelper.getString(SettingsHelper.KEY_DEVICE_NAME));
          shareIntent.putExtra(Intent.EXTRA_TEXT, shareMessage);
          startActivity(Intent.createChooser(shareIntent, getString(R.string.send_link_chooser)));
      } catch(Exception e) {
          e.printStackTrace();
          Toast.makeText(this, R.string.send_link_failed, Toast.LENGTH_LONG).show();
      }
  }
  private void showAbout() {
      ImageView imageView = new ImageView(this);
      imageView.setImageDrawable(getResources().getDrawable(R.mipmap.ic_launcher));
      new AlertDialog.Builder(this)
              .setTitle(R.string.about_title)
              .setMessage(getString(R.string.about_message, BuildConfig.VERSION_NAME, BuildConfig.VARIANT))
              .setPositiveButton(R.string.ok, (dialog, which) -> dialog.dismiss())
              .create()
              .show();
  }
  private void connect() {
      if (sessionId == null || password == null) {
          sessionId = Utils.randomString(8, true);
          password = Utils.randomString(4, true);
      }
      sharingEngine.setUsername(settingsHelper.getString(SettingsHelper.KEY_DEVICE_NAME));
      sharingEngine.connect(this, sessionId, password, (success, errorReason) -> {
          if (!success) {
              if (errorReason != null && errorReason.equals(Const.ERROR_ICE_FAILED)) {
                  errorReason = getString(R.string.connection_error_ice);
              }
              String message = getString(R.string.connection_error, settingsHelper.getString(SettingsHelper.KEY_SERVER_URL), errorReason);
              reportError(message);
              editTextSessionId.setText(null);
              editTextPassword.setText(null);
          }
      });
      scheduleExitOnIdle();
  }
  private void reportError(final String message) {

Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show();

      final AlertDialog dialog = new AlertDialog.Builder(this)
              .setMessage(message)
              .setNegativeButton(R.string.copy_message, (dialog1, which) -> {
                  ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
                  ClipData clip = ClipData.newPlainText(Const.LOG_TAG, message);
                  clipboard.setPrimaryClip(clip);
                  Toast.makeText(MainActivity.this, R.string.message_copied, Toast.LENGTH_LONG).show();
                  dialog1.dismiss();
              })
              .setPositiveButton(R.string.close, (dialog1, which) -> dialog1.dismiss())
              .create();
      try {
          dialog.show();
          handler.postDelayed(() -> {
              try {
                  dialog.dismiss();
              } catch (Exception e) {
              }
          }, 10000);
      } catch (Exception e) {
      }
  }
  @Override
  public void onStartSharing(String adminName) {
      // This event is raised when the admin joins the text room
      this.adminName = adminName;
      updateUI();
      cancelExitOnIdle();
      scheduleSharingTimeout();
      ScreenSharingHelper.requestSharing(this);
  }
  @Override
  public void onStopSharing() {
      // This event is raised when the admin leaves the text room
      notifySharingStop();
      adminName = null;
      updateUI();
      cancelSharingTimeout();
      scheduleExitOnIdle();
      ScreenSharingHelper.stopSharing(this, false);
  }
  @Override
  public void onRemoteControlEvent(String event) {
      Intent intent = new Intent(MainActivity.this, GestureDispatchService.class);
      intent.setAction(Const.ACTION_GESTURE);
      intent.putExtra(Const.EXTRA_EVENT, event);
      startService(intent);
  }
  @Override
  public void onPing() {
      if (adminName != null) {
          cancelSharingTimeout();
          scheduleSharingTimeout();
      }
  }
  @Override
  public void onSharingApiStateChanged(int state) {
      updateUI();
      if (state == Const.STATE_CONNECTED) {
          String rtpHost = Utils.getRtpUrl(settingsHelper.getString(SettingsHelper.KEY_SERVER_URL));
          int rtpAudioPort = sharingEngine.getAudioPort();
          int rtpVideoPort = sharingEngine.getVideoPort();
          String testDstIp = settingsHelper.getString(SettingsHelper.KEY_TEST_DST_IP);
          if (testDstIp != null && !testDstIp.trim().equals("")) {
              rtpHost = testDstIp;
              rtpVideoPort = Const.TEST_RTP_PORT;
              Toast.makeText(this, "Test mode: sending stream to " + rtpHost + ":" + rtpVideoPort, Toast.LENGTH_LONG).show();
          }
          ScreenSharingHelper.configure(this, settingsHelper.getBoolean(SettingsHelper.KEY_TRANSLATE_AUDIO),
                  settingsHelper.getInt(SettingsHelper.KEY_FRAME_RATE),
                  settingsHelper.getInt(SettingsHelper.KEY_BITRATE),
                  rtpHost,
                  rtpAudioPort,
                  rtpVideoPort
                  );
      }
  }
  private void scheduleExitOnIdle() {
      int exitOnIdleTimeout = settingsHelper.getInt(SettingsHelper.KEY_IDLE_TIMEOUT);
      if (exitOnIdleTimeout > 0) {
          exitCounter = EXIT_PROMPT_SEC;
          handler.postDelayed(warningOnIdleRunnable, exitOnIdleTimeout * 1000);
          Log.d(Const.LOG_TAG, "Scheduling exit in " + (exitOnIdleTimeout * 1000) + " sec");
      }
  }
  private void cancelExitOnIdle() {
      Log.d(Const.LOG_TAG, "Cancelling scheduled exit");
      handler.removeCallbacks(warningOnIdleRunnable);
      handler.removeCallbacks(exitRunnable);
  }
  private Runnable exitRunnable = () -> {
      exitCounter--;
      if (exitCounter > 0) {
          TextView messageView = exitOnIdleDialog.findViewById(android.R.id.message);
          if (messageView != null) {
              messageView.setText(MainActivity.this.getResources().getString(R.string.app_idle_warning, exitCounter));
          }
          scheduleExitRunnable();
      } else {
          gracefulExit();
      }
  };
  private Runnable warningOnIdleRunnable = () -> {
       exitOnIdleDialog = new AlertDialog.Builder(MainActivity.this)
              .setMessage(MainActivity.this.getResources().getString(R.string.app_idle_warning, exitCounter))
              .setPositiveButton(R.string.button_exit, (dialog1, which) -> {
                  gracefulExit();
              })
              .setNegativeButton(R.string.button_keep_idle, (dialog1, which) -> {
                  scheduleExitOnIdle();
                  handler.removeCallbacks(exitRunnable);
                  dialog1.dismiss();
              })
              .setCancelable(false)
              .create();
       try {
           exitOnIdleDialog.show();
           scheduleExitRunnable();
       } catch (Exception e) {
           gracefulExit();
       }
  };
  private void scheduleExitRunnable() {
      handler.postDelayed(exitRunnable, 1000);
  }
  private void scheduleSharingTimeout() {
      int pingTimeout = settingsHelper.getInt(SettingsHelper.KEY_PING_TIMEOUT);
      if (pingTimeout > 0) {
          Log.d(Const.LOG_TAG, "Scheduling sharing stop in " + (pingTimeout * 1000) + " sec");
          handler.postDelayed(sharingStopByPingTimeoutRunnable, pingTimeout * 1000);
      }
  }
  private void cancelSharingTimeout() {
      Log.d(Const.LOG_TAG, "Cancelling scheduled sharing stop");
      handler.removeCallbacks(sharingStopByPingTimeoutRunnable);
  }
  private Runnable sharingStopByPingTimeoutRunnable = new Runnable() {
      @Override
      public void run() {
          Toast.makeText(MainActivity.this, R.string.app_sharing_session_ping_timeout, Toast.LENGTH_LONG).show();
          if (adminName != null) {
              notifySharingStop();
              ScreenSharingHelper.stopSharing(MainActivity.this, false);
          }
          adminName = null;
          updateUI();
          cancelSharingTimeout();
          scheduleExitOnIdle();
          sharingEngine.disconnect(MainActivity.this, (success, errorReason) -> connect());
      }
  };
  private Runnable overlayDotRunnable = new Runnable() {
      @Override
      public void run() {
          if (overlayDotDirection == 0) {
              return;
          }
          overlayDotAlpha += OVERLAY_DOT_ANIMATION_INCREMENT * overlayDotDirection;
          if (overlayDotAlpha > 255) {
              overlayDotAlpha = 255;
              overlayDotDirection = -overlayDotDirection;
          }
          if (overlayDotAlpha < 128) {
              overlayDotAlpha = 128;
              overlayDotDirection = -overlayDotDirection;
          }
          overlayDot.setImageAlpha(overlayDotAlpha);
          handler.postDelayed(overlayDotRunnable, OVERLAY_DOT_ANIMATION_DELAY);
      }
  };
  private void notifySharingStart() {
      notifyGestureService(Const.ACTION_SCREEN_SHARING_START);
      if (settingsHelper.getBoolean(SettingsHelper.KEY_NOTIFY_SHARING)) {
          // Show a flashing dot
          Utils.lockDeviceRotation(this, true);
          overlayDot = createOverlayDot();
          overlayDotAlpha = 0;
          overlayDotDirection = 1;
          handler.postDelayed(overlayDotRunnable, OVERLAY_DOT_ANIMATION_DELAY);
      } else {
          // Just show some dialog to trigger the traffic
          final AlertDialog dialog = new AlertDialog.Builder(MainActivity.this)
                  .setMessage(R.string.share_start_text)
                  .setPositiveButton(R.string.ok, (dialog1, which) -> dialog1.dismiss())
                  .create();
          dialog.show();
          handler.postDelayed(() -> {
              if (dialog != null && dialog.isShowing()) {
                  try {
                      dialog.dismiss();
                  } catch (Exception e) {
                  }
              }
          }, 3000);
      }
  }
  private void notifySharingStop() {
      notifyGestureService(Const.ACTION_SCREEN_SHARING_STOP);
      if (settingsHelper.getBoolean(SettingsHelper.KEY_NOTIFY_SHARING)) {
          overlayDotDirection = 0;
          if (overlayDot != null) {
              WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
              wm.removeView(overlayDot);
              overlayDot = null;
          }
          Utils.lockDeviceRotation(this, false);
      }
  }
  private void notifyGestureService(String action) {
      Intent intent = new Intent(MainActivity.this, GestureDispatchService.class);
      intent.setAction(action);
      startService(intent);
  }
  public ImageView createOverlayDot() {
      int size = getResources().getDimensionPixelOffset(R.dimen.overlay_dot_size);
      WindowManager.LayoutParams params = new WindowManager.LayoutParams(size, size,
              Utils.OverlayWindowType(),
              WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                      |WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                      |WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
              PixelFormat.TRANSLUCENT);
      params.gravity = Gravity.LEFT | Gravity.TOP;
      params.x = getResources().getDimensionPixelOffset(R.dimen.overlay_dot_offset);
      params.y = getResources().getDimensionPixelOffset(R.dimen.overlay_dot_offset);
      ImageView view = new ImageView(this);
      view.setImageResource(R.drawable.flash_dot);
      view.setImageAlpha(0);
      WindowManager wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
      wm.addView(view, params);
      return view;
  }

}

Last modified: by 188.43.235.177