package me.sergiotarxz.openmg.x11; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.AlertDialog; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.PixelFormat; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.PowerManager; import android.preference.PreferenceManager; import android.provider.Settings; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.NotificationCompat; import android.util.DisplayMetrics; import android.util.Log; import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.widget.Toast; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.Iterator; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import org.json.JSONObject; @SuppressWarnings({"ConstantConditions", "SameParameterValue", "SdCardPath"}) @SuppressLint({"ClickableViewAccessibility", "StaticFieldLeak"}) public class LorieService extends Service { static final String LAUNCHED_BY_COMPATION = "me.sergiotarxz.openmg.x11.launched_by_companion"; static final String ACTION_STOP_SERVICE = "me.sergiotarxz.openmg.x11.service_stop"; static final String ACTION_START_FROM_ACTIVITY = "me.sergiotarxz.openmg.x11.start_from_activity"; static final String ACTION_START_PREFERENCES_ACTIVITY = "me.sergiotarxz.openmg.x11.start_preferences_activity"; static final String ACTION_PREFERENCES_CHAGED = "me.sergiotarxz.openmg.x11.preferences_changed"; private static LorieService instance = null; //private //static long compositor; private static final ServiceEventListener listener = new ServiceEventListener(); private static MainActivity act; private TouchParser mTP; public LorieService(){ instance = this; } static void setMainActivity(MainActivity activity) { act = activity; } static void start(String action) { Intent intent = new Intent(act, LorieService.class); intent.setAction(action); // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // act.startForegroundService(intent); // } else { act.startService(intent); // } } @SuppressLint({"BatteryLife", "ObsoleteSdkInt"}) @Override public void onCreate() { if (isServiceRunningInForeground(this, LorieService.class)) return; String datadir = getApplicationInfo().dataDir; String nativeLibDir = getApplicationInfo().nativeLibraryDir; String mappingsFile = nativeLibDir + "/libmappings.so"; String usr = "datadir"+"/files/usr"; try { if (new File(usr).exists()) { Files.walk(Paths.get(usr)) .map(Path::toFile) .sorted((o1, o2) -> -o1.compareTo(o2)) .forEach(File::delete); } InputStream is = new FileInputStream(mappingsFile); int size = is.available(); byte[] buffer = new byte[size]; is.read(buffer); is.close(); String json = new String(buffer, "UTF-8"); JSONObject mappingsObject = new JSONObject(json); Iterator keys = mappingsObject.keys(); while (keys.hasNext()) { String key = (String) keys.next(); String value = mappingsObject.getString(key); Path target; if (value.charAt(0) == '@') { // It is a lib\d+.so value = nativeLibDir+"/"+value.substring(1); } target = Paths.get(value); new File(key).getParentFile().mkdirs(); Files.createSymbolicLink(Paths.get(key), target); } } catch (Exception ex) { ex.printStackTrace(); } String[] dirs = { datadir + "/files/locale", datadir + "/files/xkb", }; for (String dir : dirs) { if (!(new File(dir)).exists()) { Log.e("LorieService", dir + " does not exist. Unpacking"); try { InputStream zipStream = getAssets().open("X11.zip"); File targetDirectory = new File(datadir + "/files/"); unzip(zipStream, targetDirectory); break; } catch (IOException e) { e.printStackTrace(); } } } compositor = createLorieThread(); if (compositor == 0) { Log.e("LorieService", "compositor thread was not created"); return; } instance = this; Toast.makeText(this, "Service was Created", Toast.LENGTH_LONG).show(); Log.e("LorieService", "created"); Intent notificationIntent = new Intent(getApplicationContext(), MainActivity.class); notificationIntent.putExtra("foo_bar_extra_key", "foo_bar_extra_value"); notificationIntent.setAction(Long.toString(System.currentTimeMillis())); Intent exitIntent = new Intent(getApplicationContext(), LorieService.class); exitIntent.setAction(ACTION_STOP_SERVICE); Intent preferencesIntent = new Intent(getApplicationContext(), LoriePreferences.class); preferencesIntent.setAction(ACTION_START_PREFERENCES_ACTIVITY); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); PendingIntent pendingExitIntent = PendingIntent.getService(getApplicationContext(), 0, exitIntent, 0); PendingIntent pendingPreferencesIntent = PendingIntent.getActivity(getApplicationContext(), 0, preferencesIntent, 0); //For creating the Foreground Service int priority = Notification.PRIORITY_HIGH; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) priority = NotificationManager.IMPORTANCE_HIGH; NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); String channelId = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? getNotificationChannel(notificationManager) : ""; Notification notification = new NotificationCompat.Builder(this, channelId) .setContentTitle("Termux:X11") .setSmallIcon(R.drawable.ic_x11_icon) .setContentText("Pull down to show options") .setContentIntent(pendingIntent) .setOngoing(true) .setPriority(priority) .setShowWhen(false) .setColor(0xFF607D8B) .addAction(0, "Preferences", pendingPreferencesIntent) .addAction(0, "Exit", pendingExitIntent) .build(); } @RequiresApi(Build.VERSION_CODES.O) private String getNotificationChannel(NotificationManager notificationManager){ String channelId = getResources().getString(R.string.app_name); String channelName = getResources().getString(R.string.app_name); NotificationChannel channel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH); channel.setImportance(NotificationManager.IMPORTANCE_NONE); channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); notificationManager.createNotificationChannel(channel); return channelId; } public static boolean isServiceRunningInForeground(Context context, Class serviceClass) { ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { if (serviceClass.getName().equals(service.service.getClassName())) { return service.foreground; } } return false; } private static void onPreferencesChanged() { if (instance == null || act == null) return; SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(instance); int mode = Integer.parseInt(preferences.getString("touchMode", "1")); instance.mTP.setMode(mode); Log.e("LorieService", "Preferences changed"); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.e("LorieService", "start"); String action = intent.getAction(); if (action.equals(ACTION_START_FROM_ACTIVITY)) { act.onLorieServiceStart(this); } if (action.equals(ACTION_STOP_SERVICE)) { Log.e("LorieService", action); terminate(); sleep(500); act.finish(); stopSelf(); System.exit(0); // This is needed to completely finish the process } onPreferencesChanged(); return START_REDELIVER_INTENT; } private static void sendRunCommandInternal(Context ctx) { // Process process = Runtime.getRuntime().exec( // "/data/data/me.sergiotarxz.openmg.x11/files/usr/bin/bash " + // "/data/data/me.sergiotarxz.openmg.x11/files/usr/bin/startopenmg" // ); // Intent intent = new Intent(); // intent.setClassName("com.termux", "com.termux.app.RunCommandService"); // intent.setAction("com.termux.RUN_COMMAND"); // intent.putExtra("com.termux.RUN_COMMAND_PATH", // "/data/data/com.termux/files/usr/libexec/termux-x11/termux-startx11"); // intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", true); // Log.d("LorieService", "sendRunCommand: " + intent); // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // ctx.startForegroundService(intent); // } else { // ctx.startService(intent); // } } public static void sendRunCommand(AppCompatActivity act) { LorieService.sendRunCommandInternal(act); } @Override public void onDestroy() { Log.e("LorieService", "destroyed"); } static LorieService getInstance() { if (instance == null) { Log.e("LorieService", "Instance was requested, but no instances available"); } return instance; } void setListeners(@NonNull SurfaceView view) { Context a = view.getRootView().findViewById(android.R.id.content).getContext(); if (!(a instanceof MainActivity)) { Log.e("LorieService", "Context is not an activity!!!"); } act = (MainActivity) a; view.setFocusable(true); view.setFocusableInTouchMode(true); view.requestFocus(); listener.svc = this; listener.setAsListenerTo(view); mTP = new TouchParser(view, listener); onPreferencesChanged(); } static View.OnKeyListener getOnKeyListener() { return listener; } void terminate() { terminate(compositor); compositor = 0; Log.e("LorieService", "terminate"); } @Override public IBinder onBind(Intent intent) { return null; } @SuppressWarnings("SameParameterValue") private static class ServiceEventListener implements SurfaceHolder.Callback, View.OnTouchListener, View.OnKeyListener, View.OnHoverListener, View.OnGenericMotionListener, TouchParser.OnTouchParseListener { LorieService svc; private void setAsListenerTo(SurfaceView view) { view.getHolder().addCallback(this); view.setOnTouchListener(this); view.setOnHoverListener(this); view.setOnGenericMotionListener(this); view.setOnKeyListener(this); surfaceChanged(view.getHolder(), PixelFormat.UNKNOWN, view.getWidth(), view.getHeight()); } public void onPointerButton(int button, int state) { if (svc == null) return; svc.pointerButton(button, state); } public void onPointerMotion(int x, int y) { if (svc == null) return; svc.pointerMotion(x, y); } public void onPointerScroll(int axis, float value) { if (svc == null) return; svc.pointerScroll(axis, value); } public void onTouchDown(int id, float x, float y) { if (svc == null) return; svc.touchDown(id, x, y); } public void onTouchMotion(int id, float x, float y) { if (svc == null) return; svc.touchMotion(id, x, y); } public void onTouchUp(int id) { if (svc == null) return; svc.touchUp(id); } public void onTouchFrame() { if (svc == null) return; svc.touchFrame(); } @Override public boolean onTouch(View v, MotionEvent e) { if (svc == null) return false; return svc.mTP.onTouchEvent(e); } @Override public boolean onGenericMotion(View v, MotionEvent e) { if (svc == null) return false; return svc.mTP.onTouchEvent(e); } @Override public boolean onHover(View v, MotionEvent e) { if (svc == null) return false; return svc.mTP.onTouchEvent(e); } private boolean isSource(KeyEvent e, int source) { return (e.getSource() & source) == source; } private boolean rightPressed = false; // Prevent right button press event from being repeated private boolean middlePressed = false; // Prevent middle button press event from being repeated @Override public boolean onKey(View v, int keyCode, KeyEvent e) { if (svc == null) return false; int action = 0; if (keyCode == KeyEvent.KEYCODE_BACK) { if ( isSource(e, InputDevice.SOURCE_MOUSE) && rightPressed != (e.getAction() == KeyEvent.ACTION_DOWN) ) { svc.pointerButton(TouchParser.BTN_RIGHT, (e.getAction() == KeyEvent.ACTION_DOWN) ? TouchParser.ACTION_DOWN : TouchParser.ACTION_UP); rightPressed = (e.getAction() == KeyEvent.ACTION_DOWN); } else if (e.getAction() == KeyEvent.ACTION_UP) { if (act.kbd!=null) act.kbd.requestFocus(); KeyboardUtils.toggleKeyboardVisibility(act); } return true; } if ( keyCode == KeyEvent.KEYCODE_MENU && isSource(e, InputDevice.SOURCE_MOUSE) && middlePressed != (e.getAction() == KeyEvent.ACTION_DOWN) ) { svc.pointerButton(TouchParser.BTN_MIDDLE, (e.getAction() == KeyEvent.ACTION_DOWN) ? TouchParser.ACTION_DOWN : TouchParser.ACTION_UP); middlePressed = (e.getAction() == KeyEvent.ACTION_DOWN); return true; } if (e.getAction() == KeyEvent.ACTION_DOWN) action = TouchParser.ACTION_DOWN; if (e.getAction() == KeyEvent.ACTION_UP) action = TouchParser.ACTION_UP; svc.keyboardKey(action, keyCode, e.isShiftPressed() ? 1 : 0, e.getCharacters()); return true; } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { DisplayMetrics dm = new DisplayMetrics(); int mmWidth, mmHeight; act.getWindowManager().getDefaultDisplay().getMetrics(dm); if (act.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { mmWidth = (int) Math.round((width * 25.4) / dm.xdpi); mmHeight = (int) Math.round((height * 25.4) / dm.ydpi); } else { mmWidth = (int) Math.round((width * 25.4) / dm.ydpi); mmHeight = (int) Math.round((height * 25.4) / dm.xdpi); } svc.windowChanged(holder.getSurface(), width, height, mmWidth, mmHeight); } @Override public void surfaceCreated(SurfaceHolder holder) {} @Override public void surfaceDestroyed(SurfaceHolder holder) {} } static void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } static final Handler handler = new Handler(); private abstract static class Task implements Runnable { public Task() { handler.post(this); } @Override public void run() { LorieService svc = getInstance(); if (svc == null || svc.compositor == 0 || act == null) { handler.postDelayed(this, 100); return; } run(svc); } public abstract void run(LorieService svc); } public static void adoptWaylandFd(int fd) { new Task() { @Override public void run(LorieService svc) { svc.passWaylandFD(fd); } }; } @SuppressWarnings("ResultOfMethodCallIgnored") public static void unzip(InputStream zipStream, File targetDirectory) throws IOException { try (ZipInputStream zis = new ZipInputStream(zipStream)) { ZipEntry ze; int count; byte[] buffer = new byte[8192]; while ((ze = zis.getNextEntry()) != null) { File file = new File(targetDirectory, ze.getName()); File dir = ze.isDirectory() ? file : file.getParentFile(); if (!dir.isDirectory() && !dir.mkdirs()) throw new FileNotFoundException("Failed to ensure directory: " + dir.getAbsolutePath()); if (ze.isDirectory()) continue; try (FileOutputStream fout = new FileOutputStream(file)) { while ((count = zis.read(buffer)) != -1) fout.write(buffer, 0, count); } long time = ze.getTime(); if (time > 0) file.setLastModified(time); } } } private void windowChanged(Surface s, int w, int h, int pw, int ph) {windowChanged(compositor, s, w, h, pw, ph);} private native void windowChanged(long compositor, Surface surface, int width, int height, int mmWidth, int mmHeight); private void touchDown(int id, float x, float y) { touchDown(compositor, id, (int) x, (int) y); } private native void touchDown(long compositor, int id, int x, int y); private void touchMotion(int id, float x, float y) { touchMotion(compositor, id, (int) x, (int) y); } private native void touchMotion(long compositor, int id, int x, int y); private void touchUp(int id) { touchUp(compositor, id); } private native void touchUp(long compositor, int id); private void touchFrame() { touchFrame(compositor); } private native void touchFrame(long compositor); private void pointerMotion(float x, float y) { pointerMotion(compositor, (int) x, (int) y); } private native void pointerMotion(long compositor, int x, int y); private void pointerScroll(int axis, float value) { pointerScroll(compositor, axis, value); } private native void pointerScroll(long compositor, int axis, float value); private void pointerButton(int button, int type) { pointerButton(compositor, button, type); } private native void pointerButton(long compositor, int button, int type); private void keyboardKey(int key, int type, int shift, String characters) {keyboardKey(compositor, key, type, shift, characters);} private native void keyboardKey(long compositor, int key, int type, int shift, String characters); private void passWaylandFD(int fd) {passWaylandFD(compositor, fd);} private native void passWaylandFD(long compositor, int fd); private native long createLorieThread(); private native void terminate(long compositor); public static native void startLogcatForFd(int fd); static { System.loadLibrary("lorie"); } }