/* * WineActivity class * * Copyright 2013-2017 Alexandre Julliard * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA */ package org.winehq.wine; import android.annotation.TargetApi; import android.app.Activity; import android.app.ProgressDialog; import android.content.Context; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.PointerIcon; import android.view.Surface; import android.view.TextureView; import android.view.View; import android.view.ViewGroup; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.Locale; import java.util.Map; public class WineActivity extends Activity { private native String wine_init( String[] cmdline, String[] env ); public native void wine_desktop_changed( int width, int height ); public native void wine_config_changed( int dpi ); public native void wine_surface_changed( int hwnd, Surface surface, boolean opengl ); public native boolean wine_motion_event( int hwnd, int action, int x, int y, int state, int vscroll ); public native boolean wine_keyboard_event( int hwnd, int action, int keycode, int state ); private final String LOGTAG = "wine"; private ProgressDialog progress_dialog; protected WineWindow desktop_window; protected WineWindow message_window; private PointerIcon current_cursor; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate( savedInstanceState ); requestWindowFeature( android.view.Window.FEATURE_NO_TITLE ); new Thread( new Runnable() { public void run() { loadWine( null ); }} ).start(); } @TargetApi(21) @SuppressWarnings("deprecation") private String[] get_supported_abis() { if (Build.VERSION.SDK_INT >= 21) return Build.SUPPORTED_ABIS; return new String[]{ Build.CPU_ABI }; } private String get_wine_abi() { for (String abi : get_supported_abis()) { File server = new File( getFilesDir(), abi + "/bin/wineserver" ); if (server.canExecute()) return abi; } Log.e( LOGTAG, "could not find a supported ABI" ); return null; } private void loadWine( String cmdline ) { copyAssetFiles(); String wine_abi = get_wine_abi(); File bindir = new File( getFilesDir(), wine_abi + "/bin" ); File libdir = new File( getFilesDir(), wine_abi + "/lib" ); File dlldir = new File( libdir, "wine" ); File prefix = new File( getFilesDir(), "prefix" ); File loader = new File( bindir, "wine" ); String locale = Locale.getDefault().getLanguage() + "_" + Locale.getDefault().getCountry() + ".UTF-8"; HashMap env = new HashMap(); env.put( "WINELOADER", loader.toString() ); env.put( "WINEPREFIX", prefix.toString() ); env.put( "WINEDLLPATH", dlldir.toString() ); env.put( "LD_LIBRARY_PATH", libdir.toString() + ":" + getApplicationInfo().nativeLibraryDir ); env.put( "LC_ALL", locale ); env.put( "LANG", locale ); env.put( "PATH", bindir.toString() + ":" + System.getenv( "PATH" )); if (cmdline == null) { if (new File( prefix, "drive_c/winestart.cmd" ).exists()) cmdline = "c:\\winestart.cmd"; else cmdline = "wineconsole.exe"; } String winedebug = readFileString( new File( prefix, "winedebug" )); if (winedebug == null) winedebug = readFileString( new File( getFilesDir(), "winedebug" )); if (winedebug != null) { File log = new File( getFilesDir(), "log" ); env.put( "WINEDEBUG", winedebug ); env.put( "WINEDEBUGLOG", log.toString() ); Log.i( LOGTAG, "logging to " + log.toString() ); log.delete(); } createProgressDialog( 0, "Setting up the Windows environment..." ); try { System.loadLibrary( "wine" ); } catch (java.lang.UnsatisfiedLinkError e) { System.load( libdir.toString() + "/libwine.so" ); } prefix.mkdirs(); runWine( cmdline, env ); } private final void runWine( String cmdline, HashMap environ ) { String[] env = new String[environ.size() * 2]; int j = 0; for (Map.Entry entry : environ.entrySet()) { env[j++] = entry.getKey(); env[j++] = entry.getValue(); } String[] cmd = { environ.get( "WINELOADER" ), "explorer.exe", "/desktop=shell,,android", cmdline }; String err = wine_init( cmd, env ); Log.e( LOGTAG, err ); } private void createProgressDialog( final int max, final String message ) { runOnUiThread( new Runnable() { public void run() { if (progress_dialog != null) progress_dialog.dismiss(); progress_dialog = new ProgressDialog( WineActivity.this ); progress_dialog.setProgressStyle( max > 0 ? ProgressDialog.STYLE_HORIZONTAL : ProgressDialog.STYLE_SPINNER ); progress_dialog.setTitle( "Wine" ); progress_dialog.setMessage( message ); progress_dialog.setCancelable( false ); progress_dialog.setMax( max ); progress_dialog.show(); }}); } private final boolean isFileWanted( String name ) { if (name.equals( "files.sum" )) return true; if (name.startsWith( "share/" )) return true; for (String abi : get_supported_abis()) { if (name.startsWith( abi + "/system/" )) return false; if (name.startsWith( abi + "/" )) return true; } if (name.startsWith( "x86/" )) return true; return false; } private final boolean isFileExecutable( String name ) { return !name.equals( "files.sum" ) && !name.startsWith( "share/" ); } private final HashMap readMapFromInputStream( InputStream in ) { HashMap map = new HashMap(); String str; try { BufferedReader reader = new BufferedReader( new InputStreamReader( in, "UTF-8" )); while ((str = reader.readLine()) != null) { String entry[] = str.split( "\\s+", 2 ); if (entry.length == 2 && isFileWanted( entry[1] )) map.put( entry[1], entry[0] ); } } catch( IOException e ) { } return map; } private final HashMap readMapFromDiskFile( String file ) { try { return readMapFromInputStream( new FileInputStream( new File( getFilesDir(), file ))); } catch( IOException e ) { return new HashMap(); } } private final HashMap readMapFromAssetFile( String file ) { try { return readMapFromInputStream( getAssets().open( file ) ); } catch( IOException e ) { return new HashMap(); } } private final String readFileString( File file ) { try { FileInputStream in = new FileInputStream( file ); BufferedReader reader = new BufferedReader( new InputStreamReader( in, "UTF-8" )); return reader.readLine(); } catch( IOException e ) { return null; } } private final void copyAssetFile( String src ) { File dest = new File( getFilesDir(), src ); try { Log.i( LOGTAG, "extracting " + dest ); dest.getParentFile().mkdirs(); dest.delete(); if (dest.createNewFile()) { InputStream in = getAssets().open( src ); FileOutputStream out = new FileOutputStream( dest ); int read; byte[] buffer = new byte[65536]; while ((read = in.read( buffer )) > 0) out.write( buffer, 0, read ); out.close(); if (isFileExecutable( src )) dest.setExecutable( true, true ); } else Log.i( LOGTAG, "Failed to create file " + dest ); } catch( IOException e ) { Log.i( LOGTAG, "Failed to copy asset file to " + dest ); dest.delete(); } } private final void deleteAssetFile( String src ) { File dest = new File( getFilesDir(), src ); Log.i( LOGTAG, "deleting " + dest ); dest.delete(); } private final void copyAssetFiles() { String new_sum = readMapFromAssetFile( "sums.sum" ).get( "files.sum" ); if (new_sum == null) return; // no assets SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( this ); String old_sum = prefs.getString( "files.sum", "" ); if (old_sum.equals( new_sum )) return; // no change prefs.edit().putString( "files.sum", new_sum ).apply(); HashMap existing_files = readMapFromDiskFile( "files.sum" ); HashMap new_files = readMapFromAssetFile( "files.sum" ); ArrayList copy_files = new ArrayList(); copy_files.add( "files.sum" ); for (Map.Entry entry : new_files.entrySet()) { String name = entry.getKey(); if (!entry.getValue().equals( existing_files.remove( name ))) copy_files.add( name ); } createProgressDialog( copy_files.size(), "Extracting files..." ); for (String name : existing_files.keySet()) deleteAssetFile( name ); for (String name : copy_files) { copyAssetFile( name ); runOnUiThread( new Runnable() { public void run() { progress_dialog.incrementProgressBy( 1 ); }}); } } // // Generic Wine window class // private HashMap win_map = new HashMap(); protected class WineWindow { static protected final int HWND_MESSAGE = 0xfffffffd; static protected final int SWP_NOZORDER = 0x04; static protected final int WS_VISIBLE = 0x10000000; protected int hwnd; protected int owner; protected int style; protected float scale; protected boolean visible; protected Rect visible_rect; protected Rect client_rect; protected WineWindow parent; protected ArrayList children; protected Surface window_surface; protected Surface client_surface; protected SurfaceTexture window_surftex; protected SurfaceTexture client_surftex; protected WineWindowGroup window_group; protected WineWindowGroup client_group; public WineWindow( int w, WineWindow parent, float scale ) { Log.i( LOGTAG, String.format( "create hwnd %08x", w )); hwnd = w; owner = 0; style = 0; visible = false; visible_rect = client_rect = new Rect( 0, 0, 0, 0 ); this.parent = parent; this.scale = scale; children = new ArrayList(); win_map.put( w, this ); if (parent != null) parent.children.add( this ); } public void destroy() { Log.i( LOGTAG, String.format( "destroy hwnd %08x", hwnd )); visible = false; win_map.remove( this ); if (parent != null) parent.children.remove( this ); destroy_window_groups(); } public WineWindowGroup create_window_groups() { if (client_group != null) return client_group; window_group = new WineWindowGroup( this ); client_group = new WineWindowGroup( this ); window_group.addView( client_group ); client_group.set_layout( client_rect.left - visible_rect.left, client_rect.top - visible_rect.top, client_rect.right - visible_rect.left, client_rect.bottom - visible_rect.top ); if (parent != null) { parent.create_window_groups(); if (visible) add_view_to_parent(); window_group.set_layout( visible_rect.left, visible_rect.top, visible_rect.right, visible_rect.bottom ); } return client_group; } public void destroy_window_groups() { if (window_group != null) { if (parent != null && parent.client_group != null) remove_view_from_parent(); window_group.destroy_view(); } if (client_group != null) client_group.destroy_view(); window_group = null; client_group = null; } public View create_whole_view() { if (window_group == null) create_window_groups(); window_group.create_view( false ).layout( 0, 0, Math.round( (visible_rect.right - visible_rect.left) * scale ), Math.round( (visible_rect.bottom - visible_rect.top) * scale )); window_group.set_scale( scale ); return window_group; } public void create_client_view() { if (client_group == null) create_window_groups(); Log.i( LOGTAG, String.format( "creating client view %08x %s", hwnd, client_rect )); client_group.create_view( true ).layout( 0, 0, client_rect.right - client_rect.left, client_rect.bottom - client_rect.top ); } protected void add_view_to_parent() { int pos = parent.client_group.getChildCount() - 1; // the content view is always last if (pos >= 0 && parent.client_group.getChildAt( pos ) == parent.client_group.get_content_view()) pos--; for (int i = 0; i < parent.children.size() && pos >= 0; i++) { WineWindow child = parent.children.get( i ); if (child == this) break; if (!child.visible) continue; if (child == ((WineWindowGroup)parent.client_group.getChildAt( pos )).get_window()) pos--; } parent.client_group.addView( window_group, pos + 1 ); } protected void remove_view_from_parent() { parent.client_group.removeView( window_group ); } protected void set_zorder( WineWindow prev ) { int pos = -1; parent.children.remove( this ); if (prev != null) pos = parent.children.indexOf( prev ); parent.children.add( pos + 1, this ); } protected void sync_views_zorder() { int i, j; for (i = parent.children.size() - 1, j = 0; i >= 0; i--) { WineWindow child = parent.children.get( i ); if (!child.visible) continue; View child_view = parent.client_group.getChildAt( j ); if (child_view == parent.client_group.get_content_view()) continue; if (child != ((WineWindowGroup)child_view).get_window()) break; j++; } while (i >= 0) { WineWindow child = parent.children.get( i-- ); if (child.visible) child.window_group.bringToFront(); } } public void pos_changed( int flags, int insert_after, int owner, int style, Rect window_rect, Rect client_rect, Rect visible_rect ) { boolean was_visible = visible; this.visible_rect = visible_rect; this.client_rect = client_rect; this.style = style; this.owner = owner; visible = (style & WS_VISIBLE) != 0; Log.i( LOGTAG, String.format( "pos changed hwnd %08x after %08x owner %08x style %08x win %s client %s visible %s flags %08x", hwnd, insert_after, owner, style, window_rect, client_rect, visible_rect, flags )); if ((flags & SWP_NOZORDER) == 0 && parent != null) set_zorder( get_window( insert_after )); if (window_group != null) { window_group.set_layout( visible_rect.left, visible_rect.top, visible_rect.right, visible_rect.bottom ); if (parent != null) { if (!was_visible && (style & WS_VISIBLE) != 0) add_view_to_parent(); else if (was_visible && (style & WS_VISIBLE) == 0) remove_view_from_parent(); else if (visible && (flags & SWP_NOZORDER) == 0) sync_views_zorder(); } } if (client_group != null) client_group.set_layout( client_rect.left - visible_rect.left, client_rect.top - visible_rect.top, client_rect.right - visible_rect.left, client_rect.bottom - visible_rect.top ); } public void set_parent( WineWindow new_parent, float scale ) { Log.i( LOGTAG, String.format( "set parent hwnd %08x parent %08x -> %08x", hwnd, parent.hwnd, new_parent.hwnd )); this.scale = scale; if (window_group != null) { if (visible) remove_view_from_parent(); new_parent.create_window_groups(); window_group.set_layout( visible_rect.left, visible_rect.top, visible_rect.right, visible_rect.bottom ); } parent.children.remove( this ); parent = new_parent; parent.children.add( this ); if (visible && window_group != null) add_view_to_parent(); } public int get_hwnd() { return hwnd; } private void update_surface( boolean is_client ) { if (is_client) { Log.i( LOGTAG, String.format( "set client surface hwnd %08x %s", hwnd, client_surface )); if (client_surface != null) wine_surface_changed( hwnd, client_surface, true ); } else { Log.i( LOGTAG, String.format( "set window surface hwnd %08x %s", hwnd, window_surface )); if (window_surface != null) wine_surface_changed( hwnd, window_surface, false ); } } public void set_surface( SurfaceTexture surftex, boolean is_client ) { if (is_client) { if (surftex == null) client_surface = null; else if (surftex != client_surftex) { client_surftex = surftex; client_surface = new Surface( surftex ); } } else { if (surftex == null) window_surface = null; else if (surftex != window_surftex) { window_surftex = surftex; window_surface = new Surface( surftex ); } } update_surface( is_client ); } public void get_event_pos( MotionEvent event, int[] pos ) { pos[0] = Math.round( event.getX() * scale + window_group.getLeft() ); pos[1] = Math.round( event.getY() * scale + window_group.getTop() ); } } // // Window group for a Wine window, optionally containing a content view // protected class WineWindowGroup extends ViewGroup { private WineView content_view; private WineWindow win; WineWindowGroup( WineWindow win ) { super( WineActivity.this ); this.win = win; setVisibility( View.VISIBLE ); } /* wrapper for layout() making sure that the view is not empty */ public void set_layout( int left, int top, int right, int bottom ) { left *= win.scale; top *= win.scale; right *= win.scale; bottom *= win.scale; if (right <= left + 1) right = left + 2; if (bottom <= top + 1) bottom = top + 2; layout( left, top, right, bottom ); } @Override protected void onLayout( boolean changed, int left, int top, int right, int bottom ) { if (content_view != null) content_view.layout( 0, 0, right - left, bottom - top ); } public void set_scale( float scale ) { if (content_view == null) return; content_view.setPivotX( 0 ); content_view.setPivotY( 0 ); content_view.setScaleX( scale ); content_view.setScaleY( scale ); } public WineView create_view( boolean is_client ) { if (content_view != null) return content_view; content_view = new WineView( WineActivity.this, win, is_client ); addView( content_view ); if (!is_client) { content_view.setFocusable( true ); content_view.setFocusableInTouchMode( true ); } return content_view; } public void destroy_view() { if (content_view == null) return; removeView( content_view ); content_view = null; } public WineView get_content_view() { return content_view; } public WineWindow get_window() { return win; } } // View used for all Wine windows, backed by a TextureView protected class WineView extends TextureView implements TextureView.SurfaceTextureListener { private WineWindow window; private boolean is_client; public WineView( Context c, WineWindow win, boolean client ) { super( c ); window = win; is_client = client; setSurfaceTextureListener( this ); setVisibility( VISIBLE ); setOpaque( false ); setFocusable( true ); setFocusableInTouchMode( true ); } public WineWindow get_window() { return window; } @Override public void onSurfaceTextureAvailable( SurfaceTexture surftex, int width, int height ) { Log.i( LOGTAG, String.format( "onSurfaceTextureAvailable win %08x %dx%d %s", window.hwnd, width, height, is_client ? "client" : "whole" )); window.set_surface( surftex, is_client ); } @Override public void onSurfaceTextureSizeChanged( SurfaceTexture surftex, int width, int height ) { Log.i( LOGTAG, String.format( "onSurfaceTextureSizeChanged win %08x %dx%d %s", window.hwnd, width, height, is_client ? "client" : "whole" )); window.set_surface( surftex, is_client); } @Override public boolean onSurfaceTextureDestroyed( SurfaceTexture surftex ) { Log.i( LOGTAG, String.format( "onSurfaceTextureDestroyed win %08x %s", window.hwnd, is_client ? "client" : "whole" )); window.set_surface( null, is_client ); return false; // hold on to the texture since the app may still be using it } @Override public void onSurfaceTextureUpdated(SurfaceTexture surftex) { } @TargetApi(24) public PointerIcon onResolvePointerIcon( MotionEvent event, int index ) { return current_cursor; } @Override public boolean onGenericMotionEvent( MotionEvent event ) { if (is_client) return false; // let the whole window handle it if (window.parent != null && window.parent != desktop_window) return false; // let the parent handle it if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { int[] pos = new int[2]; window.get_event_pos( event, pos ); Log.i( LOGTAG, String.format( "view motion event win %08x action %d pos %d,%d buttons %04x view %d,%d", window.hwnd, event.getAction(), pos[0], pos[1], event.getButtonState(), getLeft(), getTop() )); return wine_motion_event( window.hwnd, event.getAction(), pos[0], pos[1], event.getButtonState(), (int)event.getAxisValue(MotionEvent.AXIS_VSCROLL) ); } return super.onGenericMotionEvent(event); } @Override public boolean onTouchEvent( MotionEvent event ) { if (is_client) return false; // let the whole window handle it if (window.parent != null && window.parent != desktop_window) return false; // let the parent handle it int[] pos = new int[2]; window.get_event_pos( event, pos ); Log.i( LOGTAG, String.format( "view touch event win %08x action %d pos %d,%d buttons %04x view %d,%d", window.hwnd, event.getAction(), pos[0], pos[1], event.getButtonState(), getLeft(), getTop() )); return wine_motion_event( window.hwnd, event.getAction(), pos[0], pos[1], event.getButtonState(), 0 ); } @Override public boolean dispatchKeyEvent( KeyEvent event ) { Log.i( LOGTAG, String.format( "view key event win %08x action %d keycode %d (%s)", window.hwnd, event.getAction(), event.getKeyCode(), KeyEvent.keyCodeToString( event.getKeyCode() )));; boolean ret = wine_keyboard_event( window.hwnd, event.getAction(), event.getKeyCode(), event.getMetaState() ); if (!ret) ret = super.dispatchKeyEvent(event); return ret; } } // The top-level desktop view group protected class TopView extends ViewGroup { public TopView( Context context, int hwnd ) { super( context ); desktop_window = new WineWindow( hwnd, null, 1.0f ); addView( desktop_window.create_whole_view() ); desktop_window.client_group.bringToFront(); message_window = new WineWindow( WineWindow.HWND_MESSAGE, null, 1.0f ); message_window.create_window_groups(); } @Override protected void onSizeChanged( int width, int height, int old_width, int old_height ) { Log.i( LOGTAG, String.format( "desktop size %dx%d", width, height )); wine_desktop_changed( width, height ); } @Override protected void onLayout( boolean changed, int left, int top, int right, int bottom ) { // nothing to do } } protected WineWindow get_window( int hwnd ) { return win_map.get( hwnd ); } // Entry points for the device driver public void create_desktop_window( int hwnd ) { Log.i( LOGTAG, String.format( "create desktop view %08x", hwnd )); setContentView( new TopView( this, hwnd )); progress_dialog.dismiss(); wine_config_changed( getResources().getConfiguration().densityDpi ); } public void create_window( int hwnd, boolean opengl, int parent, float scale, int pid ) { WineWindow win = get_window( hwnd ); if (win == null) { win = new WineWindow( hwnd, get_window( parent ), scale ); win.create_window_groups(); if (win.parent == desktop_window) win.create_whole_view(); } if (opengl) win.create_client_view(); } public void destroy_window( int hwnd ) { WineWindow win = get_window( hwnd ); if (win != null) win.destroy(); } public void set_window_parent( int hwnd, int parent, float scale, int pid ) { WineWindow win = get_window( hwnd ); if (win == null) return; win.set_parent( get_window( parent ), scale ); if (win.parent == desktop_window) win.create_whole_view(); } @TargetApi(24) public void set_cursor( int id, int width, int height, int hotspotx, int hotspoty, int bits[] ) { Log.i( LOGTAG, String.format( "set_cursor id %d size %dx%d hotspot %dx%d", id, width, height, hotspotx, hotspoty )); if (bits != null) { Bitmap bitmap = Bitmap.createBitmap( bits, width, height, Bitmap.Config.ARGB_8888 ); current_cursor = PointerIcon.create( bitmap, hotspotx, hotspoty ); } else current_cursor = PointerIcon.getSystemIcon( this, id ); } public void window_pos_changed( int hwnd, int flags, int insert_after, int owner, int style, Rect window_rect, Rect client_rect, Rect visible_rect ) { WineWindow win = get_window( hwnd ); if (win != null) win.pos_changed( flags, insert_after, owner, style, window_rect, client_rect, visible_rect ); } public void createDesktopWindow( final int hwnd ) { runOnUiThread( new Runnable() { public void run() { create_desktop_window( hwnd ); }} ); } public void createWindow( final int hwnd, final boolean opengl, final int parent, final float scale, final int pid ) { runOnUiThread( new Runnable() { public void run() { create_window( hwnd, opengl, parent, scale, pid ); }} ); } public void destroyWindow( final int hwnd ) { runOnUiThread( new Runnable() { public void run() { destroy_window( hwnd ); }} ); } public void setParent( final int hwnd, final int parent, final float scale, final int pid ) { runOnUiThread( new Runnable() { public void run() { set_window_parent( hwnd, parent, scale, pid ); }} ); } public void setCursor( final int id, final int width, final int height, final int hotspotx, final int hotspoty, final int bits[] ) { if (Build.VERSION.SDK_INT < 24) return; runOnUiThread( new Runnable() { public void run() { set_cursor( id, width, height, hotspotx, hotspoty, bits ); }} ); } public void windowPosChanged( final int hwnd, final int flags, final int insert_after, final int owner, final int style, final int window_left, final int window_top, final int window_right, final int window_bottom, final int client_left, final int client_top, final int client_right, final int client_bottom, final int visible_left, final int visible_top, final int visible_right, final int visible_bottom ) { final Rect window_rect = new Rect( window_left, window_top, window_right, window_bottom ); final Rect client_rect = new Rect( client_left, client_top, client_right, client_bottom ); final Rect visible_rect = new Rect( visible_left, visible_top, visible_right, visible_bottom ); runOnUiThread( new Runnable() { public void run() { window_pos_changed( hwnd, flags, insert_after, owner, style, window_rect, client_rect, visible_rect ); }} ); } }