Differences
This shows you the differences between the selected revisions of the page.
| history 2022-09-12 | history 2026-09-11 (current) | ||
| Line 1: | Line 1: | ||
| - | /* | + | ====== Recent Version History ====== |
| - | * Headwind Remote: Open Source Remote Access Software for Android | + | |
| - | * https://headwind-remote.com | + | |
| - | * | + | |
| - | * Copyright (C) 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; | + | This is a full list of changes for each release of WinSCP. See also [[project_history|Project history]] and [[incompatible_changes|Incompatible changes between versions]]. |
| - | import android.app.AlertDialog; | + | ===== [[6.8]] 6.8 (not released yet) ((2026-08-22)) ===== |
| - | 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; | + | * Word wrapping in the internal editor can be toggled from toolbar menu. [[bug>2451]] |
| - | import androidx.appcompat.widget.Toolbar; | + | |
| - | import androidx.localbroadcastmanager.content.LocalBroadcastManager; | + | |
| - | import com.hmdm.control.janus.SharingEngineJanus; | + | ===== [[6.6.4]] 6.6.4 (not released yet) ((2026-09-11)) ===== |
| + | |||
| + | * Increased length limit of proxy host name for updates preferences. [[bug>2456]] | ||
| + | * Optionally default to keeping Login dialog open after opening session in PuTTY. [[bug>2459]] | ||
| + | * Improving placement of widows, particularly those opened, while the main window is not visible (notably during command-line operations). | ||
| + | * Bug fix: When selecting a site to perform a command-line operation with, it was possible to select a workspace or a folder, resulting in unexpected behaviour or failure. [[bug>2457]] | ||
| + | * Bug fix: After opening session in PuTTY, WinSCP process is never closed. [[bug>2458]] | ||
| + | * Bug fix: Invalid language code for Serbian was used for Microsoft Store package. [[bug>2460]] | ||
| - | public class MainActivity extends AppCompatActivity implements SharingEngineJanus.EventListener, SharingEngineJanus.StateListener { | + | ===== [[6.6.3]] 6.6.3 RC ((2026-09-03)) ===== |
| - | ····private ImageView imageViewConnStatus; | + | ··* Translations completed: Belarusian, Brazilian Portuguese, Catalan, Croatian, Czech, Danish, Dutch, Finnish, French, German, Hungarian, Italian, Japanese, Korean, Lithuanian, Macedonian, Polish, Portuguese, Romanian, Russian, Serbian, Simplified Chinese, Slovak, Slovenian, Spanish, Swedish, Tamil, Traditional Chinese and Turkish. |
| - | private TextView textViewConnStatus; | + | * Some parts of GUI (panel headers, scrollbars, buttons, checkboxes) show dark in dark theme even when system-wide app theme is light. |
| - | private EditText editTextSessionId; | + | * SSH core and private key tools (PuTTYgen and Pageant) upgraded to [[&url(puttychanges)|PuTTY 0.85]]. \\ It brings the following change: |
| - | private EditText editTextPassword; | + | * Security issue: fixed a remotely triggerable use-after-free in Pageant. [[pbug>pageant-deferred-decryption-uaf]] |
| - | private TextView textViewComment; | + | * Security issue: fixed a remotely triggerable buffer overflow if the OpenSSH encrypt-then-MAC cipher modes are in use. [[pbug>etm-large-packet-overflow]] |
| - | ···private TextView textViewConnect; | + | * Security issue: fixed a buffer overflow in private key decryption, if the private key is constructed maliciously. [[pbug>argon2-parameter-checks]] |
| - | ···private TextView textViewSendLink; | + | * Denial-of-service security fixes: a server can trigger a tight loop in WinSCP, and even a MITM can make it consume unlimited memory at startup. [[pbug>maxpkt-0-tight-loop]] |
| - | ···private TextView textViewExit; | + | ·* Bundled SSH private key tools (PuTTYgen and Pageant) are 64-bit. |
| + | * On Windows 11, using system dark tab theme, that uses different shades for active and disabled/disconnected tabs. [[bug>2452]] | ||
| + | ·* TLS/SSL core upgraded to OpenSSL 3.5.8. | ||
| + | * XML parser upgraded to Expat 2.8.4. | ||
| + | * Source code package build script supports 64-bit target. | ||
| + | * Installer upgraded to Inno Setup 6.7.3. | ||
| + | * 64-bit build is identified in version information. | ||
| + | * Resolving version of pwsh installed with MSIX. | ||
| + | * Thirdparty information from the About dialog can be copied to the clipboard even when the browser control malfunctions. | ||
| + | * Bug fix: No error is shown when connection fails. | ||
| + | * Bug fix: ';'x-name'' URL parameter was incorrectly decoded. | ||
| + | ·* Bug fix: Incorrect number validation. | ||
| + | * Bug fix: Some controls (notably list view headers) do not correctly apply dark mode in 64-bit build. [[bug>2453]] | ||
| + | * Bug fix: 64-bit build did not identify its system to be 64-bit, what among other prevented it from identifying 64-bit COM registrations. | ||
| + | * Bug fix: Some strings use incorrect translation in 64-bit version. [[bug>2455]] | ||
| - | ····private ImageView overlayDot; | + | ===== [[6.6.2]] 6.6.2 RC ((2026-06-17)) ===== |
| - | ····private Handler handler = new Handler(); | + | |
| - | ···private int overlayDotAlpha; | + | |
| - | ····private int overlayDotDirection = 1; | + | |
| - | ····private Dialog exitOnIdleDialog; | + | ··* Experimental 64-bit version of WinSCP. [[bug>618]] |
| - | private int exitCounter; | + | * Optionally not showing error message when connection is lost while idle. [[bug>2360]] |
| - | private static final int EXIT_PROMPT_SEC = 10; | + | * SSH core and SSH private key tools (PuTTYgen and Pageant) upgraded to [[&url(puttychanges)|PuTTY 0.84]]. \\ It brings the following changes: |
| + | * Security issue: fixed a remotely triggerable double-free in RSA key exchange. [[pbug>rsakex-double-free]] | ||
| + | * Minor security issue: fixed a remotely triggerable crash in NIST ECDSA signature verification. [[pbug>ecdsa-remotely-triggerable-assertion]] | ||
| + | * Bug fix: spurious //"Network error: Socket is not connected"// when authenticating to some HTTP proxies. [[pbug>http-proxy-auth-wsaenotconn]] | ||
| + | * TLS/SSL core upgraded to OpenSSL 3.5.7. | ||
| + | * XML parser upgraded to Expat 2.8.1. | ||
| + | * Restored faster C TLS/SSL AES implementation. | ||
| + | * Configurable warning when opening large file in an internal editor. [[bug>2437]] | ||
| + | * Informing that when preserving directory timestamps is enabled, using multiple connections for transfer is not possible. [[bug>2439]] | ||
| + | * Warning when pasting a session URL with unsafe settings. | ||
| + | * When opening session in PuTTY to a host for which WinSCP has multiple host keys cached, using the last key or the key that PuTTY has cached. [[bug>2440]] | ||
| + | * Always (re)registering drag&drop shell extension during installation, even when the extension is not replaced. | ||
| + | * Allowed Console interface tool to have ''.exe'' extension to avoid false positive detections by some antiviruses. [[bug>2434]] | ||
| + | * Using //"username"// and //"hostname"// as one word. | ||
| + | * Reading all system settings from 64-bit registry. | ||
| + | * Allow assigning ''null'' to ''Session.SessionLogPath''. [[bug>2438]] | ||
| + | * Avoiding using ''SSH_FXF_EXCL'' together with ''SSH_FXF_TRUNC'' SFTP file opening flags. [[bug>2444]] | ||
| + | * Optimized file system monitoring when looking for dummy directory during drag&drop downloads. [[bug>2445]] | ||
| + | * Change: Not allowing WebDAV redirects to other hosts by default. [[bug>2447]] | ||
| + | * Change: Not allowing WebDAV redirects to an unencrypted URL by default. [[bug>2448]] | ||
| + | * Updated to JCL library 2.9 commit c669fd12. | ||
| + | * Bug fix: Failure when trying to connect via HTTP proxy to FTP host with excessively long login details. [[bug>2435]] | ||
| + | * Bug fix: Buffer overflow in Console interface tool. [[bug>2436]] | ||
| + | * Bug fix: Failure setting ''Session.DebugLogPath'' when running in impersonated context. [[bug>2441]] | ||
| + | * Bug fix: Message boxes from secondary windows (like the internal editor) caused application to move to the background when when the main window was minimized. [[bug>2443]] | ||
| + | * Bug fix: Heap over-read via crafted encrypted filename. [[bug>2449]] | ||
| + | * Bug fix: Slashes in filenames can cause path traversal when invalid filename characters replacement is disabled. [[bug>2450]] | ||
| - | ····private static final int OVERLAY_DOT_ANIMATION_INCREMENT = 20; | + | ===== [[6.6.1]] 6.6.1 beta ((2026-04-01)) ===== |
| - | ···private static final int OVERLAY_DOT_ANIMATION_DELAY = 200; | + | |
| - | ····private SharingEngine sharingEngine; | + | ··* Support for OpenSSH ssh-agent. [[bug>1682]] |
| + | * Optionally connecting all workspace/folder sessions immediately. [[bug>1026]] | ||
| + | * Preserving panel scroll position after rename. [[bug>2425]] | ||
| + | * ''Ctrl+C'' works in list views on 'Server and protocol information' dialog. | ||
| + | * Preventing moving or copying a file or folder over ancestor folder with the same name. [[bug>2427]] | ||
| + | * WebDAV/HTTP core upgraded to neon 0.37.1. | ||
| + | * XML parser upgraded to Expat 2.7.5. | ||
| + | * Bug fix: Some menus were not working on displays to the left or above the primary display. [[bug>2423]] | ||
| + | * Bug fix: Mouse wheel downwards scrolling did not work on toolbar drop down lists. | ||
| + | * Bug fix: Once any control of permissions popup box was focused the popup no longer closed when user clicked outside of it. | ||
| + | * Bug fix: Failure when closing Transfer settings dialog with //X// button while a permissions popup box control is focused. [[bug>2420]] | ||
| + | * Bug fix: Failure when switching to a session that is being reconnected. | ||
| + | * Bug fix: Failure when the first bit of an SFTP response is set. [[bug>2422]] | ||
| + | * Bug fix: Copying to clipboard with ''Ctrl+C'' from 'Server and protocol information' was broken. | ||
| + | * Bug fix: Protocol additional information scrolling was broken. | ||
| + | * Bug fix: Master password dialog was missing //Help// button. | ||
| + | * Bug fix: Checking if edited/opened file was modified externally didn't work for inactive sessions. [[bug>2426]] | ||
| + | * Bug fix: Wrapped settings values from Raw Site Settings dialog were not preserved. | ||
| + | * Bug fix: Some files modified by local custom command in SCP session fail to upload back. [[bug>2428]] | ||
| + | * Bug fix: Whole //Key exchange// page was incorrectly hidden when //"Handles SSH key re-exchange badly"// bug was enabled. | ||
| + | * Bug fix: Some message boxes leak GDI handle. [[bug>2430]] | ||
| + | * Bug fix: Login dialog leaks GDI handles. [[bug>2431]] | ||
| - | ····private SettingsHelper settingsHelper; | + | ===== [[6.6]] 6.6 beta ((2026-02-02)) ===== |
| - | ····private String sessionId; | + | ··* Synchronizing two local directories. [[bug>2020]] |
| - | private String password; | + | * Compiler upgraded to Clang/bcc32c. [[bug>618]] |
| - | private String adminName; | + | * Inactive sessions can be automatically reconnected. [[bug>2232]] |
| + | * Added dark theme support to: [[bug>1696]] | ||
| + | * Login dialog. [[bug>2345]] | ||
| + | * Transfer Options dialog. | ||
| + | * Message boxes. | ||
| + | * Queue column headers. [[bug>2356]] | ||
| + | * Progress window. | ||
| + | * Authentication Progress window. [[bug>2358]] | ||
| + | * Bug fix: Scrollbar colors did not always reflect the color theme | ||
| + | * Using modern directory selection dialog that scales correctly and allows creating new directory. [[bug>2373]] [[bug>2389]] | ||
| + | * Optimized GUI when working with large subdirectory selection. [[bug>2396]] | ||
| + | * Change: Default to UTF-8 encoding in internal editor. [[bug>2397]] | ||
| + | * New DigiCert EV code signing certificate valid until March 2029 is used for signing binaries. | ||
| + | * TLS/SSL core upgraded to OpenSSL 3.5.5. | ||
| + | * WebDAV/HTTP core upgraded to neon 0.36.0. | ||
| + | * XML parser upgraded to Expat 2.7.4. | ||
| + | * Installer upgraded to Inno Setup 6.7.0 with dark mode support enabled. | ||
| + | * Increased WinSCP memory limit to 4 GB. [[bug>2412]] | ||
| + | * Defined and implemented interface for the .NET library. By @mjkent. [[bug>856]] | ||
| + | * Optimized TLS/SSL AES implementation. | ||
| + | * Restoring ability to restart Explorer to allow upgrade of drag&drop shell extension, when installing for current user, as after-restart replacement is not possible without Administrator privileges. [[bug>2381]] | ||
| + | * MSI toolset updated to WiX 5. | ||
| + | * Commands to copy paths to the clipboard on the Synchronization checklist window. | ||
| + | * Cryptography optimization. | ||
| + | * Support long AWS/S3 session tokens. [[bug>2403]] | ||
| + | * Prevent hang when new device is attached or removed while some mapped network drive is not available. [[bug>2382]] | ||
| + | * Copy and paste improvements: | ||
| + | * Consistently renaming local files dropped or pasted back to their source directory to avoid collisions. | ||
| + | * Bug fix: When copying local files to clipboard from system context menu, "cut" state of previously cut files was not cleared. | ||
| + | * Not redundantly verifying WebDAV or S3 certificate in Windows Certificate store if it is already marked as trusted in session settings. [[bug>2404]] | ||
| + | * Provide SNI when opening FTP data connection. [[bug>2410]] | ||
| + | * Optimized synchronization checklist sorting. | ||
| + | * Support for Beyond Compare 5 in Compare Files extension. [[bug>2417]] | ||
| + | * Convert unsupported SSH proxy to SSH tunnel when importing site from PuTTY. [[bug>2408]] | ||
| + | * FTP directory listing falls back to the other active/passive mode, consistently with file transfers. | ||
| + | * Consistently calling command to open window with specific directory //Explore//, instead of sometimes //Browse//. | ||
| + | * Consistently referring to file last modification timestamp column as //Date modified//. | ||
| + | * With INI file provided on command-line, using the same INI file when starting a new instance. | ||
| + | * Windows shell local file copy status window is centered on the main window. | ||
| + | * Made taskbar flashing configurable in GUI. [[bug>2411]] | ||
| + | * Control labels on transfer settings dialogs do not show keyboard accelerator cue, until ''Alt'' key is pressed. | ||
| + | * Not using drag images even with directory trees. [[bug>1274]] | ||
| + | * Allow configuring checksum commands. [[bug>2394]] | ||
| + | * Updated to JCL library 2.8.1. | ||
| + | * Updating jump list only when running with GUI. | ||
| + | * Made space on permissions box for longer translations. [[bug>2398]] | ||
| + | * Opening //Default Apps// //Settings// page directly to open it in the foreground and avoid flashing //Control Panel// window. | ||
| + | * Improving order in which Windows Narrator reads window controls. | ||
| + | * All edit boxes with history consistently do not auto complete and show 16 entries in the drop down. | ||
| + | * Removed obsolete //Preserve remote timestamp// session settings. | ||
| + | * Bug fix: Local file with invalid characters replaced could not be explored from the Synchronization checklist window. | ||
| + | * Bug fix: Files modified by local custom command are not always uploaded to the correct remote directory. [[bug>2370]] | ||
| + | * Bug fix: List of network drives in drive drop down and directory tree did not always match. | ||
| + | * Bug fix: Host key prompt did not have the default button. | ||
| + | * Bug fix: When the local path specified on Open directory/Location profile dialog is not existing, when browsing for a new path, the trailing part of the nonexisting path was appended to the new path. | ||
| + | * Bug fix: Trying to enter an invalid link in local panel fails silently. | ||
| + | * Bug fix: After FTP data connection fails to open further use of the session is broken. | ||
| + | * Bug fix: Pasting cut files from the clipboard into a local panel copies them instead of moving them. [[bug>2400]] | ||
| + | * Bug fix: Some edits did not save their value to history when submitting with ''Enter''. | ||
| + | * Bug fix: Too long edit history dropdown can overflow monitor bounds. [[bug>2432]] | ||
| + | * Bug fix: Message box texts and some control labels are not visible to screen readers. [[bug>2413]] | ||
| + | * Bug fix: Failure when clicking tab close button while the session is already being closed. [[bug>2416]] | ||
| - | ····private final static String ATTR_SESSION_ID = "sessionId"; | + | ===== [[6.5.9]] 6.5.9 (not released yet) ((2026-09-10)) ===== |
| - | ···private final static String ATTR_PASSWORD = "password"; | + | |
| - | ···private final static String ATTR_ADMIN_NAME = "adminName"; | + | |
| - | ····private boolean needReconnect = false; | + | ··* Back-propagated fixes from 6.6.4 release: |
| + | * Bug fix: Invalid language code for Serbian was used for Microsoft Store package. [[bug>2460]] | ||
| - | ····private MediaProjectionManager projectionManager; | + | ===== [[6.5.8]] 6.5.8 ((2026-09-10)) ===== |
| - | ····private BroadcastReceiver mSharingServiceReceiver = new BroadcastReceiver() { | + | ··* This is Microsoft Store-only release that fixes packaging problem of 6.5.7. The actual binaries are still 6.5.7. |
| - | @Override | + | ···* Bug fix: Cannot install from Microsoft Store because of invalid 'sr' language. [[bug>2460]] |
| - | 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)) { | + | ===== [[6.5.7]] 6.5.7 ((2026-09-09)) ===== |
| - | ················notifySharingStop(); | + | |
| - | adminName = null; | + | |
| - | updateUI(); | + | |
| - | ················cancelSharingTimeout(); | + | |
| - | ···············scheduleExitOnIdle(); | + | |
| - | ············} else if (intent.getAction().equals(Const.ACTION_SCREEN_SHARING_FAILED)) { | + | ··* Translations completed: Croatian, Finnish, Georgian, Italian and Serbian, and updated: Slovenian. |
| - | ···············String message = intent.getStringExtra(Const.EXTRA_MESSAGE); | + | ··* TLS/SSL core upgraded to OpenSSL 3.3.7. |
| - | ···············if (message != null) { | + | * SSH private key tools (PuTTYgen and Pageant) upgraded to [[&url(puttychanges)|PuTTY 0.85]]. SSH core upgraded to include some fixes. \\ It brings the following change: |
| - | ···················Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show(); | + | ···* Security issue: fixed a remotely triggerable use-after-free in Pageant. [[pbug>pageant-deferred-decryption-uaf]] |
| - | ···············} | + | ···* Security issue: fixed a remotely triggerable buffer overflow if the OpenSSH encrypt-then-MAC cipher modes are in use. [[pbug>etm-large-packet-overflow]] |
| - | ···············adminName = null; | + | ···* Security issue: fixed a buffer overflow in private key decryption, if the private key is constructed maliciously. [[pbug>argon2-parameter-checks]] |
| - | ···············updateUI(); | + | * Denial-of-service security fixes: a server can trigger a tight loop in WinSCP, and even a MITM can make it consume unlimited memory at startup. [[pbug>maxpkt-0-tight-loop]] |
| - | ···············cancelSharingTimeout(); | + | * Security issue: fixed a remotely triggerable double-free in RSA key exchange. [[pbug>rsakex-double-free]] |
| - | ···············scheduleExitOnIdle(); | + | ···* Minor security issue: fixed a remotely triggerable crash in NIST ECDSA signature verification. [[pbug>ecdsa-remotely-triggerable-assertion]] |
| + | ·* Back-propagated fixes from 6.6.2 beta release: | ||
| + | ···* Bug fix: Failure setting ''Session.DebugLogPath'' when running in impersonated context. [[bug>2441]] | ||
| + | ·* Bug fix: A specially crafted PKCS#7 or S/MIME signed message could trigger a use-after-free during PKCS#7 signature verification. CVE-2026-45447 fix from OpenSSL 3.4.6. | ||
| - | ············} else if (intent.getAction().equals(Const.ACTION_CONNECTION_FAILURE)) { | + | ===== [[6.5.6]] 6.5.6 ((2026-03-25)) ===== |
| - | 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)) { | + | ··* Translations completed: Macedonian, and updated: Lithuanian, and Russian. |
| - | ···············startActivityForResult(projectionManager.createScreenCaptureIntent(), Const.REQUEST_SCREEN_SHARE); | + | ··* TLS/SSL core upgraded to OpenSSL 3.3.6. |
| - | ···········} | + | ··* Back-propagated improvements from 6.6–6.6.1 beta release: |
| - | ·······} | + | ···* New DigiCert EV code signing certificate valid until March 2029 is used for signing binaries. |
| - | ···}; | + | * XML parser upgraded to Expat 2.7.5. |
| + | * Support for Beyond Compare 5 in Compare Files extension. [[bug>2417]] | ||
| + | ···* Bug fix: Checking if edited/opened file was modified externally didn';t work for inactive sessions. [[bug>2426]] | ||
| - | ····@Override | + | ===== [[6.5.5]] 6.5.5 ((2025-11-19)) ===== |
| - | ····protected void onCreate(Bundle savedInstanceState) { | + | |
| - | ········super.onCreate(savedInstanceState); | + | |
| - | ········setContentView(R.layout.activity_main); | + | |
| - | ········if (savedInstanceState != null) { | + | ··* Translation updated: Vietnamese. |
| - | ···········restoreInstanceState(savedInstanceState); | + | · * Bug fix: Pasting files using local directory tree context menu pastes them to the current directory, instead of the selected one. |
| - | ·······} | + | ·* Bug fix: Failure when opening site imported from PuTTY with unsupported SSH proxy. [[bug>2407]] |
| + | * Bug fix: Incorrect hostname validation when connecting to S3 endpoint with certificate that does not cover root S3 hostname. [[bug>2409]] | ||
| - | ········Toolbar toolbar = findViewById(R.id.toolbar); | + | ===== [[6.5.4]] 6.5.4 ((2025-10-16)) ===== |
| - | ·······setSupportActionBar(toolbar); | + | |
| - | settingsHelper = SettingsHelper.getInstance(this); | + | |
| - | ········sharingEngine = SharingEngineFactory.getSharingEngine(); | + | |
| - | sharingEngine.setEventListener(this); | + | |
| - | ·······sharingEngine.setStateListener(this); | + | |
| - | ········DisplayMetrics metrics = new DisplayMetrics(); | + | ··* Translations updated: Belarusian and Georgian. |
| - | ScreenSharingHelper.getRealScreenSize(this, metrics); | + | * TLS/SSL core upgraded to OpenSSL 3.3.5. |
| - | float videoScale = ScreenSharingHelper.adjustScreenMetrics(metrics); | + | * XML parser upgraded to Expat 2.7.3. |
| - | settingsHelper.setFloat(SettingsHelper.KEY_VIDEO_SCALE, videoScale); | + | * Added new ''ap-southeast-6'' AWS region. |
| - | ScreenSharingHelper.setScreenMetrics(this, metrics.widthPixels, metrics.heightPixels, metrics.densityDpi); | + | * Bug fix: When restored after operation completed while minimized the window is disabled. [[bug>2393]] |
| - | + | * Bug fix: Command ''md5sums'' is incorrectly used to calculate MD5 checksum instead of ''md5sum''. [[bug>2392]] | |
| - | sharingEngine.setScreenWidth(metrics.widthPixels); | + | * Bug fix: Incomplete FTP upload when the source stream/stdin reads less than requested. [[bug>2395]] |
| - | sharingEngine.setScreenHeight(metrics.heightPixels); | + | * Bug fix: ''Shift''-clicking //OK// button on Synchronization checklist window when synchronization in the background was not possible still closed the window. |
| - | + | * Bug fix: Failure after reloading file panel when number of files decreases. [[bug>2402]] | |
| - | IntentFilter intentFilter = new IntentFilter(Const.ACTION_SCREEN_SHARING_START); | + | * Bug fix: Failure or silently missing headers when when S3 request headers were too long. |
| - | 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; | + | |
| - | } | + | |
| - | } | + | |
| + | [[history_old|Older versions]] | ||
| + | ~~NOTOC~~ | ||
| + | ~~ARCHIVE=history_old~~ | ||