diff options
Diffstat (limited to 'android/xscreensaver/src/org/jwz')
9 files changed, 2245 insertions, 0 deletions
diff --git a/android/xscreensaver/src/org/jwz/xscreensaver/Activity.java b/android/xscreensaver/src/org/jwz/xscreensaver/Activity.java new file mode 100644 index 0000000..ac0ab4c --- /dev/null +++ b/android/xscreensaver/src/org/jwz/xscreensaver/Activity.java @@ -0,0 +1,169 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * xscreensaver, Copyright (c) 2016 Jamie Zawinski <jwz@jwz.org> + * and Dennis Sheil <dennis@panaceasupplies.com> + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. No representations are made about the suitability of this + * software for any purpose. It is provided "as is" without express or + * implied warranty. + * + * This is the XScreenSaver "application" that just brings up the + * Live Wallpaper preferences. + */ + +package org.jwz.xscreensaver; + +import android.app.WallpaperManager; +import android.content.ComponentName; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.provider.Settings; +import android.Manifest; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.os.Build; +import android.content.pm.PackageManager; + +public class Activity extends android.app.Activity + implements View.OnClickListener { + + private boolean wallpaperButtonClicked, daydreamButtonClicked; + private final static int MY_REQ_READ_EXTERNAL_STORAGE = 271828; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // openList(); + setContentView(R.layout.activity_xscreensaver); + wallpaperButtonClicked = false; + daydreamButtonClicked = false; + + findViewById(R.id.apply_wallpaper).setOnClickListener(this); + findViewById(R.id.apply_daydream).setOnClickListener(this); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + case R.id.apply_wallpaper: + wallpaperButtonClicked(); + break; + case R.id.apply_daydream: + daydreamButtonClicked(); + break; + } + } + + // synchronized when dealing with wallpaper state - perhaps can + // narrow down more + private synchronized void withProceed() { + if (daydreamButtonClicked) { + String action; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + action = Settings.ACTION_DREAM_SETTINGS; + } else { + action = Settings.ACTION_DISPLAY_SETTINGS; + } + startActivity(new Intent(action)); + } else if (wallpaperButtonClicked) { + startActivity(new Intent(WallpaperManager.ACTION_LIVE_WALLPAPER_CHOOSER)); + } + } + + private void wallpaperButtonClicked() { + wallpaperButtonClicked = true; + checkPermission(); + } + + private void daydreamButtonClicked() { + daydreamButtonClicked = true; + checkPermission(); + } + + void checkPermission() { + // RES introduced in API 16 + String permission = Manifest.permission.READ_EXTERNAL_STORAGE; + if (havePermission(permission)) { + withProceed(); + } else { + noPermission(permission); + } + } + + private void noPermission(String permission) { + int myRequestCode; + myRequestCode = MY_REQ_READ_EXTERNAL_STORAGE; + + if (permissionsDeniedRationale(permission)) { + showDeniedRationale(); + } else { + requestPermission(permission, myRequestCode); + } + } + + private boolean permissionsDeniedRationale(String permission) { + boolean rationale = ActivityCompat.shouldShowRequestPermissionRationale(this, + permission); + return rationale; + } + + private void requestPermission(String permission, int myRequestCode) { + ActivityCompat.requestPermissions(this, + new String[]{permission}, + myRequestCode); + + // myRequestCode is an app-defined int constant. + // The callback method gets the result of the request. + } + + // TODO: This method should be asynchronous, and not block the thread + private void showDeniedRationale() { + withProceed(); + } + + boolean havePermission(String permission) { + + if (Build.VERSION.SDK_INT < 16) { + return true; + } + + if (permissionGranted(permission)) { + return true; + } + + return false; + } + + private boolean permissionGranted(String permission) { + boolean check = ContextCompat.checkSelfPermission(this, permission) == + PackageManager.PERMISSION_GRANTED; + return check; + } + + public void proceedIfPermissionGranted(int[] grantResults) { + + // If request is cancelled, the result arrays are empty. + if (grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + withProceed(); + } else if (grantResults.length > 0) { + withProceed(); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, + String permissions[], int[] grantResults) { + switch (requestCode) { + case MY_REQ_READ_EXTERNAL_STORAGE: + proceedIfPermissionGranted(grantResults); + } + } + +} diff --git a/android/xscreensaver/src/org/jwz/xscreensaver/App.java b/android/xscreensaver/src/org/jwz/xscreensaver/App.java new file mode 100644 index 0000000..3d39788 --- /dev/null +++ b/android/xscreensaver/src/org/jwz/xscreensaver/App.java @@ -0,0 +1,22 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * xscreensaver, Copyright (c) 2016 Jamie Zawinski <jwz@jwz.org> + * and Dennis Sheil <dennis@panaceasupplies.com> + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. No representations are made about the suitability of this + * software for any purpose. It is provided "as is" without express or + * implied warranty. + */ + +package org.jwz.xscreensaver; + +import android.app.Application; + +public class App extends Application { + public App() { + super(); + } +} diff --git a/android/xscreensaver/src/org/jwz/xscreensaver/Daydream.java b/android/xscreensaver/src/org/jwz/xscreensaver/Daydream.java new file mode 100644 index 0000000..372af95 --- /dev/null +++ b/android/xscreensaver/src/org/jwz/xscreensaver/Daydream.java @@ -0,0 +1,269 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * xscreensaver, Copyright (c) 2016-2017 Jamie Zawinski <jwz@jwz.org> + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. No representations are made about the suitability of this + * software for any purpose. It is provided "as is" without express or + * implied warranty. + * + * The superclass of every saver's Daydream. + * + * Each Daydream needs a distinct subclass in order to show up in the list. + * We know which saver we are running by the subclass name; we know which + * API to use by how the subclass calls super(). + */ + +package org.jwz.xscreensaver; + +import android.view.Display; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.view.KeyEvent; +import android.service.dreams.DreamService; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.os.Message; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +public class Daydream extends DreamService { + + private class SaverView extends SurfaceView + implements SurfaceHolder.Callback { + + private boolean initTried = false; + private jwxyz jwxyz_obj; + + private GestureDetector detector; + + private Runnable on_quit = new Runnable() { + @Override + public void run() { + finish(); // Exit the Daydream + } + }; + + SaverView () { + super (Daydream.this); + getHolder().addCallback(this); + } + + @Override + public void surfaceChanged (SurfaceHolder holder, int format, + int width, int height) { + + if (width == 0 || height == 0) { + detector = null; + jwxyz_obj.close(); + jwxyz_obj = null; + } + + Log.d ("xscreensaver", + String.format("surfaceChanged: %dx%d", width, height)); + + /* + double r = 0; + + Display d = view.getDisplay(); + + if (d != null) { + switch (d.getRotation()) { + case Surface.ROTATION_90: r = 90; break; + case Surface.ROTATION_180: r = 180; break; + case Surface.ROTATION_270: r = 270; break; + } + } + */ + + if (jwxyz_obj == null) { + jwxyz_obj = new jwxyz (jwxyz.saverNameOf (Daydream.this), + Daydream.this, screenshot, width, height, + holder.getSurface(), on_quit); + detector = new GestureDetector (Daydream.this, jwxyz_obj); + } else { + jwxyz_obj.resize (width, height); + } + + jwxyz_obj.start(); + } + + @Override + public void surfaceCreated (SurfaceHolder holder) { + if (!initTried) { + initTried = true; + } else { + if (jwxyz_obj != null) { + jwxyz_obj.close(); + jwxyz_obj = null; + } + } + } + + @Override + public void surfaceDestroyed (SurfaceHolder holder) { + if (jwxyz_obj != null) { + jwxyz_obj.close(); + jwxyz_obj = null; + } + } + + @Override + public boolean onTouchEvent (MotionEvent event) { + detector.onTouchEvent (event); + if (event.getAction() == MotionEvent.ACTION_UP) + jwxyz_obj.dragEnded (event); + return true; + } + + @Override + public boolean onKeyDown (int keyCode, KeyEvent event) { + // In the emulator, this doesn't receive keyboard arrow keys, PgUp, etc. + // Some other keys like "Home" are interpreted before we get here, and + // function keys do weird shit. + + // TODO: Does this still work? And is the above still true? + + if (view.jwxyz_obj != null) + view.jwxyz_obj.sendKeyEvent (event); + return true; + } + } + + private SaverView view; + Bitmap screenshot; + + private void LOG (String fmt, Object... args) { + Log.d ("xscreensaver", + this.getClass().getSimpleName() + ": " + + String.format (fmt, args)); + } + + protected Daydream () { + super(); + } + + // Called when jwxyz_abort() is called, or other exceptions are thrown. + // +/* + @Override + public void uncaughtException (Thread thread, Throwable ex) { + + renderer = null; + String err = ex.toString(); + LOG ("Caught exception: %s", err); + + this.finish(); // Exit the Daydream + + final AlertDialog.Builder b = new AlertDialog.Builder(this); + b.setMessage (err); + b.setCancelable (false); + b.setPositiveButton ("Bummer", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, int id) { + } + }); + + // #### This isn't working: + // "Attempted to add window with non-application token" + // "Unable to add window -- token null is not for an application" + // I think I need to get an "Activity" to run it on somehow? + + new Handler (Looper.getMainLooper()).post (new Runnable() { + public void run() { + AlertDialog alert = b.create(); + alert.setTitle (this.getClass().getSimpleName() + " crashed"); + alert.setIcon(android.R.drawable.ic_dialog_alert); + alert.show(); + } + }); + + old_handler.uncaughtException (thread, ex); + } +*/ + + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + setInteractive (true); + setFullscreen (true); + saveScreenshot(); + + view = new SaverView (); + setContentView (view); + } + + public void onDreamingStarted() { + super.onDreamingStarted(); + // view.jwxyz_obj.start(); + } + + public void onDreamingStopped() { + super.onDreamingStopped(); + view.jwxyz_obj.pause(); + } + + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + try { + if (view.jwxyz_obj != null) + view.jwxyz_obj.pause(); + } catch (Exception exc) { + // Fun fact: Android swallows exceptions coming from here, then crashes + // elsewhere. + LOG ("onDetachedFromWindow: %s", exc.toString()); + throw exc; + } + } + + + // At startup, before we have blanked the screen, save a screenshot + // for later use by the hacks. + // + private void saveScreenshot() { + View view = getWindow().getDecorView().getRootView(); + if (view == null) { + LOG ("unable to get root view for screenshot"); + } else { + + // This doesn't work: + /* + boolean was = view.isDrawingCacheEnabled(); + if (!was) view.setDrawingCacheEnabled (true); + view.buildDrawingCache(); + screenshot = view.getDrawingCache(); + if (!was) view.setDrawingCacheEnabled (false); + if (screenshot == null) { + LOG ("unable to get screenshot bitmap from %s", view.toString()); + } else { + screenshot = Bitmap.createBitmap (screenshot); + } + */ + + // This doesn't work either: width and height are both -1... + + int w = view.getLayoutParams().width; + int h = view.getLayoutParams().height; + if (w <= 0 || h <= 0) { + LOG ("unable to get root view for screenshot"); + } else { + screenshot = Bitmap.createBitmap (w, h, Bitmap.Config.ARGB_8888); + Canvas c = new Canvas (screenshot); + view.layout (0, 0, w, h); + view.draw (c); + } + } + } +} diff --git a/android/xscreensaver/src/org/jwz/xscreensaver/Settings.java b/android/xscreensaver/src/org/jwz/xscreensaver/Settings.java new file mode 100644 index 0000000..17bac0f --- /dev/null +++ b/android/xscreensaver/src/org/jwz/xscreensaver/Settings.java @@ -0,0 +1,179 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * xscreensaver, Copyright (c) 2016 Jamie Zawinski <jwz@jwz.org> + * and Dennis Sheil <dennis@panaceasupplies.com> + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. No representations are made about the suitability of this + * software for any purpose. It is provided "as is" without express or + * implied warranty. + * + * The superclass of every saver's preferences panel. + * + * The only reason the subclasses of this class exist is so that we know + * which "_settings.xml" to read -- we extract the base name from self's + * class. + * + * project/xscreensaver/res/xml/SAVER_dream.xml refers to it as + * android:settingsActivity="SAVER_Settings". If there was some way + * to pass an argument from the XML into here, or to otherwise detect + * which Dream was instantiating this Settings, we wouldn't need those + * hundreds of Settings subclasses. + */ + +package org.jwz.xscreensaver; + +import android.content.SharedPreferences; +import android.os.Bundle; + +import android.content.SharedPreferences; +import android.preference.PreferenceActivity; +import android.preference.Preference; +import android.preference.ListPreference; +import android.preference.EditTextPreference; +import android.preference.CheckBoxPreference; +import org.jwz.xscreensaver.SliderPreference; + +import org.jwz.xscreensaver.R; +import java.util.Map; +import java.lang.reflect.Field; + +public abstract class Settings extends PreferenceActivity + implements SharedPreferences.OnSharedPreferenceChangeListener { + + @Override + protected void onCreate (Bundle icicle) { + super.onCreate (icicle); + + // Extract the saver name from e.g. "BouncingCowSettings" + String name = this.getClass().getSimpleName(); + String tail = "Settings"; + if (name.endsWith(tail)) + name = name.substring (0, name.length() - tail.length()); + name = name.toLowerCase(); + + // #### All of these have been deprecated: + // getPreferenceManager() + // addPreferencesFromResource(int) + // findPreference(CharSequence) + + getPreferenceManager().setSharedPreferencesName (name); + + // read R.xml.SAVER_settings dynamically + int res = -1; + String pref_class = name + "_settings"; + try { res = R.xml.class.getDeclaredField(pref_class).getInt (null); } + catch (Exception e) { } + if (res != -1) + addPreferencesFromResource (res); + + final int res_final = res; + + SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); + prefs.registerOnSharedPreferenceChangeListener (this); + updateAllPrefsSummaries (prefs); + + // Find the "Reset to defaults" button and install a click handler on it. + // + Preference reset = findPreference (name + "_reset"); + reset.setOnPreferenceClickListener( + new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + + SharedPreferences prefs = + getPreferenceManager().getSharedPreferences(); + + // Wipe everything from the preferences hash, then reload defaults. + prefs.edit().clear().commit(); + getPreferenceScreen().removeAll(); + addPreferencesFromResource (res_final); + + // I guess we need to re-get this after the removeAll? + prefs = getPreferenceManager().getSharedPreferences(); + + // But now we need to iterate over every Preference widget and + // push the new value down into it. If you think this all looks + // ridiculously non-object-oriented and completely insane, that's + // because it is. + + Map <String, ?> keys = prefs.getAll(); + for (Map.Entry <String, ?> entry : keys.entrySet()) { + String key = entry.getKey(); + String val = String.valueOf (entry.getValue()); + + Preference pref = findPreference (key); + if (pref instanceof ListPreference) { + ((ListPreference) pref).setValue (prefs.getString (key, "")); + } else if (pref instanceof SliderPreference) { + ((SliderPreference) pref).setValue (prefs.getFloat (key, 0)); + } else if (pref instanceof EditTextPreference) { + ((EditTextPreference) pref).setText (prefs.getString (key, "")); + } else if (pref instanceof CheckBoxPreference) { + ((CheckBoxPreference) pref).setChecked ( + prefs.getBoolean (key,false)); + } + + updatePrefsSummary (prefs, pref); + } + return true; + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); + prefs.registerOnSharedPreferenceChangeListener (this); + updateAllPrefsSummaries(prefs); + } + + @Override + protected void onPause() { + getPreferenceManager().getSharedPreferences(). + unregisterOnSharedPreferenceChangeListener(this); + super.onPause(); + } + + @Override + protected void onDestroy() { + getPreferenceManager().getSharedPreferences(). + unregisterOnSharedPreferenceChangeListener(this); + super.onDestroy(); + } + + public void onSharedPreferenceChanged (SharedPreferences sharedPreferences, + String key) { + updatePrefsSummary(sharedPreferences, findPreference(key)); + } + + protected void updatePrefsSummary(SharedPreferences sharedPreferences, + Preference pref) { + if (pref == null) + return; + + if (pref instanceof ListPreference) { + pref.setTitle (((ListPreference) pref).getEntry()); + } else if (pref instanceof SliderPreference) { + float v = ((SliderPreference) pref).getValue(); + int i = (int) Math.floor (v); + if (v == i) + pref.setSummary (String.valueOf (i)); + else + pref.setSummary (String.valueOf (v)); + } else if (pref instanceof EditTextPreference) { + pref.setSummary (((EditTextPreference) pref).getText()); + } + } + + protected void updateAllPrefsSummaries(SharedPreferences prefs) { + + Map <String, ?> keys = prefs.getAll(); + for (Map.Entry <String, ?> entry : keys.entrySet()) { + updatePrefsSummary (prefs, findPreference (entry.getKey())); + } + } +} diff --git a/android/xscreensaver/src/org/jwz/xscreensaver/SliderPreference.java b/android/xscreensaver/src/org/jwz/xscreensaver/SliderPreference.java new file mode 100644 index 0000000..c1a1a1d --- /dev/null +++ b/android/xscreensaver/src/org/jwz/xscreensaver/SliderPreference.java @@ -0,0 +1,160 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * xscreensaver, Copyright (c) 2016 Jamie Zawinski <jwz@jwz.org> + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. No representations are made about the suitability of this + * software for any purpose. It is provided "as is" without express or + * implied warranty. + * + * A numeric preference as a slider, inline in the preferences list. + * XML options include: + * + * low, high (floats) -- smallest and largest allowed values. + * If low > high, the value increases as the slider's thumb moves left. + * + * lowLabel, highLabel (strings) -- labels shown at the left and right + * ends of the slider. + * + * integral (boolean) -- whether to use whole numbers instead of floats; + */ + +package org.jwz.xscreensaver; + +import android.content.Context; +import android.content.res.TypedArray; +import android.content.res.Resources; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.SeekBar; +import android.widget.TextView; +import android.util.Log; + +public class SliderPreference extends Preference { + + protected float low, high; + protected String low_label, high_label; + protected boolean integral; + protected float mValue; + protected int seekbar_ticks; + + public SliderPreference(Context context, AttributeSet attrs) { + this (context, attrs, 0); + } + + public SliderPreference (Context context, AttributeSet attrs, int defStyle) { + super (context, attrs, defStyle); + + Resources res = context.getResources(); + + // Parse these from the "<SliderPreference>" tag + low = Float.parseFloat (attrs.getAttributeValue (null, "low")); + high = Float.parseFloat (attrs.getAttributeValue (null, "high")); + integral = attrs.getAttributeBooleanValue (null, "integral", false); + low_label = res.getString( + attrs.getAttributeResourceValue (null, "lowLabel", 0)); + high_label = res.getString( + attrs.getAttributeResourceValue (null, "highLabel", 0)); + + seekbar_ticks = (integral + ? (int) Math.floor (Math.abs (high - low)) + : 100000); + + setWidgetLayoutResource (R.layout.slider_preference); + } + + + @Override + protected void onSetInitialValue (boolean restore, Object def) { + if (restore) { + mValue = getPersistedFloat (low); + } else { + mValue = (Float) def; + persistFloat (mValue); + } + //Log.d("xscreensaver", String.format("SLIDER INIT %s: %f", + // low_label, mValue)); + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getFloat (index, low); + } + + + public float getValue() { + return mValue; + } + + public void setValue (float value) { + + if (low < high) { + value = Math.max (low, Math.min (high, value)); + } else { + value = Math.max (high, Math.min (low, value)); + } + + if (integral) + value = Math.round (value); + + if (value != mValue) { + //Log.d("xscreensaver", String.format("SLIDER %s: %f", low_label, value)); + persistFloat (value); + mValue = value; + notifyChanged(); + } + } + + + @Override + protected View onCreateView (ViewGroup parent) { + View view = super.onCreateView(parent); + + TextView low_view = (TextView) + view.findViewById (R.id.slider_preference_low); + low_view.setText (low_label); + + TextView high_view = (TextView) + view.findViewById (R.id.slider_preference_high); + high_view.setText (high_label); + + SeekBar seekbar = (SeekBar) + view.findViewById (R.id.slider_preference_seekbar); + seekbar.setMax (seekbar_ticks); + + float ratio = (mValue - low) / (high - low); + int seek_value = (int) (ratio * (float) seekbar_ticks); + + seekbar.setProgress (seek_value); + + final SliderPreference slider = this; + + seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onProgressChanged (SeekBar seekBar, int progress, + boolean fromUser) { + if (fromUser) { + float ratio = (float) progress / (float) seekbar_ticks; + float value = low + (ratio * (high - low)); + slider.setValue (value); + callChangeListener (progress); + } + } + }); + + return view; + } +} diff --git a/android/xscreensaver/src/org/jwz/xscreensaver/TTFAnalyzer.java b/android/xscreensaver/src/org/jwz/xscreensaver/TTFAnalyzer.java new file mode 100644 index 0000000..3d01345 --- /dev/null +++ b/android/xscreensaver/src/org/jwz/xscreensaver/TTFAnalyzer.java @@ -0,0 +1,153 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + + * Copyright (C) 2011 George Yunaev @ Ulduzsoft + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + + http://www.ulduzsoft.com/2012/01/enumerating-the-fonts-on-android-platform/ + */ + +package org.jwz.xscreensaver; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.HashMap; + +// The class which loads the TTF file, parses it and returns the TTF font name +class TTFAnalyzer +{ + // This function parses the TTF file and returns the font name specified in the file + public String getTtfFontName( String fontFilename ) + { + try + { + // Parses the TTF file format. + // See http://developer.apple.com/fonts/ttrefman/rm06/Chap6.html + m_file = new RandomAccessFile( fontFilename, "r" ); + + // Read the version first + int version = readDword(); + + // The version must be either 'true' (0x74727565) or 0x00010000 or 'OTTO' (0x4f54544f) for CFF style fonts. + if ( version != 0x74727565 && version != 0x00010000 && version != 0x4f54544f) + return null; + + // The TTF file consist of several sections called "tables", and we need to know how many of them are there. + int numTables = readWord(); + + // Skip the rest in the header + readWord(); // skip searchRange + readWord(); // skip entrySelector + readWord(); // skip rangeShift + + // Now we can read the tables + for ( int i = 0; i < numTables; i++ ) + { + // Read the table entry + int tag = readDword(); + readDword(); // skip checksum + int offset = readDword(); + int length = readDword(); + + // Now here' the trick. 'name' field actually contains the textual string name. + // So the 'name' string in characters equals to 0x6E616D65 + if ( tag == 0x6E616D65 ) + { + // Here's the name section. Read it completely into the allocated buffer + byte[] table = new byte[ length ]; + + m_file.seek( offset ); + read( table ); + + // This is also a table. See http://developer.apple.com/fonts/ttrefman/rm06/Chap6name.html + // According to Table 36, the total number of table records is stored in the second word, at the offset 2. + // Getting the count and string offset - remembering it's big endian. + int count = getWord( table, 2 ); + int string_offset = getWord( table, 4 ); + + // Record starts from offset 6 + for ( int record = 0; record < count; record++ ) + { + // Table 37 tells us that each record is 6 words -> 12 bytes, and that the nameID is 4th word so its offset is 6. + // We also need to account for the first 6 bytes of the header above (Table 36), so... + int nameid_offset = record * 12 + 6; + int platformID = getWord( table, nameid_offset ); + int nameid_value = getWord( table, nameid_offset + 6 ); + + // Table 42 lists the valid name Identifiers. We're interested in 4 but not in Unicode encoding (for simplicity). + // The encoding is stored as PlatformID and we're interested in Mac encoding + if ( nameid_value == 4 && platformID == 1 ) + { + // We need the string offset and length, which are the word 6 and 5 respectively + int name_length = getWord( table, nameid_offset + 8 ); + int name_offset = getWord( table, nameid_offset + 10 ); + + // The real name string offset is calculated by adding the string_offset + name_offset = name_offset + string_offset; + + // Make sure it is inside the array + if ( name_offset >= 0 && name_offset + name_length < table.length ) + return new String( table, name_offset, name_length ); + } + } + } + } + + return null; + } + catch (FileNotFoundException e) + { + // Permissions? + return null; + } + catch (IOException e) + { + // Most likely a corrupted font file + return null; + } + } + + // Font file; must be seekable + private RandomAccessFile m_file = null; + + // Helper I/O functions + private int readByte() throws IOException + { + return m_file.read() & 0xFF; + } + + private int readWord() throws IOException + { + int b1 = readByte(); + int b2 = readByte(); + + return b1 << 8 | b2; + } + + private int readDword() throws IOException + { + int b1 = readByte(); + int b2 = readByte(); + int b3 = readByte(); + int b4 = readByte(); + + return b1 << 24 | b2 << 16 | b3 << 8 | b4; + } + + private void read( byte [] array ) throws IOException + { + if ( m_file.read( array ) != array.length ) + throw new IOException(); + } + + // Helper + private int getWord( byte [] array, int offset ) + { + int b1 = array[ offset ] & 0xFF; + int b2 = array[ offset + 1 ] & 0xFF; + + return b1 << 8 | b2; + } +} diff --git a/android/xscreensaver/src/org/jwz/xscreensaver/TVActivity.java b/android/xscreensaver/src/org/jwz/xscreensaver/TVActivity.java new file mode 100644 index 0000000..0015c9d --- /dev/null +++ b/android/xscreensaver/src/org/jwz/xscreensaver/TVActivity.java @@ -0,0 +1,50 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * xscreensaver, Copyright (c) 2017 Jamie Zawinski <jwz@jwz.org> + * and Dennis Sheil <dennis@panaceasupplies.com> + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. No representations are made about the suitability of this + * software for any purpose. It is provided "as is" without express or + * implied warranty. + * + * This is the XScreenSaver "application" that just brings up the + * Daydream preferences for Android TV. + */ + +package org.jwz.xscreensaver; + +import android.app.Activity; +import android.app.WallpaperManager; +import android.content.ComponentName; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.provider.Settings; + +public class TVActivity extends Activity + implements View.OnClickListener { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_tv_xscreensaver); + findViewById(R.id.apply_daydream).setOnClickListener(this); + } + + @Override + public void onClick(View v) { + switch (v.getId()) { + + case R.id.apply_daydream: + String action; + Intent intent = new Intent(android.provider.Settings.ACTION_SETTINGS); + startActivityForResult(intent, 0); + break; + } + } +} diff --git a/android/xscreensaver/src/org/jwz/xscreensaver/Wallpaper.java b/android/xscreensaver/src/org/jwz/xscreensaver/Wallpaper.java new file mode 100644 index 0000000..93896f2 --- /dev/null +++ b/android/xscreensaver/src/org/jwz/xscreensaver/Wallpaper.java @@ -0,0 +1,128 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * xscreensaver, Copyright (c) 2016-2018 Jamie Zawinski <jwz@jwz.org> + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. No representations are made about the suitability of this + * software for any purpose. It is provided "as is" without express or + * implied warranty. + * + * The superclass of every saver's Wallpaper. + * + * Each Wallpaper needs a distinct subclass in order to show up in the list. + * We know which saver we are running by the subclass name; we know which + * API to use by how the subclass calls super(). + */ + +package org.jwz.xscreensaver; + +import android.content.res.Configuration; +import android.service.wallpaper.WallpaperService; +import android.view.GestureDetector; +import android.view.SurfaceHolder; +import android.util.Log; +import java.lang.RuntimeException; +import java.lang.Thread; +import org.jwz.xscreensaver.jwxyz; +import android.graphics.PixelFormat; +import android.view.WindowManager; +import android.view.Display; +import android.graphics.Point; + +public class Wallpaper extends WallpaperService +/*implements GestureDetector.OnGestureListener, + GestureDetector.OnDoubleTapListener, */ { + + /* TODO: Input! */ + private Engine engine; + + @Override + public Engine onCreateEngine() { + // Log.d("xscreensaver", "tid = " + Thread.currentThread().getId()); + engine = new XScreenSaverGLEngine(); + return engine; + } + + @Override + public void onConfigurationChanged(Configuration config) { + super.onConfigurationChanged(config); + Log.d("xscreensaver", "wallpaper onConfigurationChanged"); + /* + WindowManager wm = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + int width = size.x; + int height = size.y; + engine.onSurfaceChanged(engine.getSurfaceHolder(), PixelFormat.RGBA_8888, width, height); + */ + + } + + class XScreenSaverGLEngine extends Engine { + + private boolean initTried = false; + private jwxyz jwxyz_obj; + + @Override + public void onSurfaceCreated (SurfaceHolder holder) { + super.onSurfaceCreated(holder); + + if (!initTried) { + initTried = true; + } else { + if (jwxyz_obj != null) { + jwxyz_obj.close(); + jwxyz_obj = null; + } + } + } + + @Override + public void onVisibilityChanged(final boolean visible) { + if (jwxyz_obj != null) { + if (visible) + jwxyz_obj.start(); + else + jwxyz_obj.pause(); + } + } + + @Override + public void onSurfaceChanged (SurfaceHolder holder, int format, + int width, int height) { + + super.onSurfaceChanged(holder, format, width, height); + + if (width == 0 || height == 0) { + jwxyz_obj.close(); + jwxyz_obj = null; + } + + Log.d ("xscreensaver", + String.format("surfaceChanged: %dx%d", width, height)); + + if (jwxyz_obj == null) { + jwxyz_obj = new jwxyz (jwxyz.saverNameOf(Wallpaper.this), + Wallpaper.this, null, width, height, + holder.getSurface(), null); + } else { + jwxyz_obj.resize (width, height); + } + + jwxyz_obj.start(); + } + + @Override + public void onSurfaceDestroyed (SurfaceHolder holder) { + super.onSurfaceDestroyed (holder); + + if (jwxyz_obj != null) { + jwxyz_obj.close(); + jwxyz_obj = null; + } + } + } +} diff --git a/android/xscreensaver/src/org/jwz/xscreensaver/jwxyz.java b/android/xscreensaver/src/org/jwz/xscreensaver/jwxyz.java new file mode 100644 index 0000000..a22a26d --- /dev/null +++ b/android/xscreensaver/src/org/jwz/xscreensaver/jwxyz.java @@ -0,0 +1,1115 @@ +/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * xscreensaver, Copyright (c) 2016-2018 Jamie Zawinski <jwz@jwz.org> + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. No representations are made about the suitability of this + * software for any purpose. It is provided "as is" without express or + * implied warranty. + * + * This class is how the C implementation of jwxyz calls back into Java + * to do things that OpenGL does not have access to without Java-based APIs. + * It is the Java companion to jwxyz-android.c and screenhack-android.c. + */ + +package org.jwz.xscreensaver; + +import java.util.Map; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.ArrayList; +import java.util.Random; +import android.app.AlertDialog; +import android.view.KeyEvent; +import android.content.SharedPreferences; +import android.content.Context; +import android.content.ContentResolver; +import android.content.DialogInterface; +import android.content.res.AssetManager; +import android.graphics.Typeface; +import android.graphics.Rect; +import android.graphics.Paint; +import android.graphics.Paint.FontMetrics; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.net.Uri; +import android.view.GestureDetector; +import android.view.KeyEvent; +import android.view.MotionEvent; +import java.net.URL; +import java.nio.ByteBuffer; +import java.io.File; +import java.io.InputStream; +import java.io.FileOutputStream; +import java.lang.InterruptedException; +import java.lang.Runnable; +import java.lang.Thread; +import java.util.TimerTask; +import android.database.Cursor; +import android.provider.MediaStore; +import android.provider.MediaStore.MediaColumns; +import android.media.ExifInterface; +import org.jwz.xscreensaver.TTFAnalyzer; +import android.util.Log; +import android.view.Surface; +import android.Manifest; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.os.Build; +import android.content.pm.PackageManager; + +public class jwxyz + implements GestureDetector.OnGestureListener, + GestureDetector.OnDoubleTapListener { + + private class PrefListener + implements SharedPreferences.OnSharedPreferenceChangeListener { + + @Override + public void onSharedPreferenceChanged (SharedPreferences sharedPreferences, String key) + { + if (key.startsWith(hack + "_")) { + if (render != null) { + boolean was_animating; + synchronized (render) { + was_animating = animating_p; + } + close(); + if (was_animating) + start(); + } + } + } + }; + + private static class SurfaceLost extends Exception { + SurfaceLost () { + super("surface lost"); + } + + SurfaceLost (String detailMessage) { + super(detailMessage); + } + } + + public final static int STYLE_BOLD = 1; + public final static int STYLE_ITALIC = 2; + public final static int STYLE_MONOSPACE = 4; + + public final static int FONT_FAMILY = 0; + public final static int FONT_FACE = 1; + public final static int FONT_RANDOM = 2; + + public final static int MY_REQ_READ_EXTERNAL_STORAGE = 271828; + + private long nativeRunningHackPtr; + + private String hack; + private Context app; + private Bitmap screenshot; + + SharedPreferences prefs; + SharedPreferences.OnSharedPreferenceChangeListener pref_listener; + Hashtable<String, String> defaults = new Hashtable<String, String>(); + + + // Maps font names to either: String (system font) or Typeface (bundled). + private Hashtable<String, Object> all_fonts = + new Hashtable<String, Object>(); + + int width, height; + Surface surface; + boolean animating_p; + + // Doubles as the mutex controlling width/height/animating_p. + private Thread render; + + private Runnable on_quit; + boolean button_down_p; + + // These are defined in jwxyz-android.c: + // + private native long nativeInit (String hack, + Hashtable<String,String> defaults, + int w, int h, Surface window) + throws SurfaceLost; + private native void nativeResize (int w, int h, double rot); + private native long nativeRender (); + private native void nativeDone (); + public native void sendButtonEvent (int x, int y, boolean down); + public native void sendMotionEvent (int x, int y); + public native void sendKeyEvent (boolean down_p, int code, int mods); + + private void LOG (String fmt, Object... args) { + Log.d ("xscreensaver", hack + ": " + String.format (fmt, args)); + } + + static public String saverNameOf (Object obj) { + // Extract the saver name from e.g. "gen.Daydream$BouncingCow" + String name = obj.getClass().getSimpleName(); + int index = name.lastIndexOf('$'); + if (index != -1) { + index++; + name = name.substring (index, name.length() - index); + } + return name.toLowerCase(); + } + + // Constructor + public jwxyz (String hack, Context app, Bitmap screenshot, int w, int h, + Surface surface, Runnable on_quit) { + + this.hack = hack; + this.app = app; + this.screenshot = screenshot; + this.on_quit = on_quit; + this.width = w; + this.height = h; + this.surface = surface; + + // nativeInit populates 'defaults' with the default values for keys + // that are not overridden by SharedPreferences. + + prefs = app.getSharedPreferences (hack, 0); + + // Keep a strong reference to pref_listener, because + // registerOnSharedPreferenceChangeListener only uses a weak reference. + pref_listener = new PrefListener(); + prefs.registerOnSharedPreferenceChangeListener (pref_listener); + + scanSystemFonts(); + } + + protected void finalize() { + if (render != null) { + LOG ("jwxyz finalized without close. This might be OK."); + close(); + } + } + + + public String getStringResource (String name) { + + name = hack + "_" + name; + + if (prefs.contains(name)) { + + // SharedPreferences is very picky that you request the exact type that + // was stored: if it is a float and you ask for a string, you get an + // exception instead of the float converted to a string. + + String s = null; + try { return prefs.getString (name, ""); + } catch (Exception e) { } + + try { return Float.toString (prefs.getFloat (name, 0)); + } catch (Exception e) { } + + try { return Long.toString (prefs.getLong (name, 0)); + } catch (Exception e) { } + + try { return Integer.toString (prefs.getInt (name, 0)); + } catch (Exception e) { } + + try { return (prefs.getBoolean (name, false) ? "true" : "false"); + } catch (Exception e) { } + } + + // If we got to here, it's not in there, so return the default. + return defaults.get (name); + } + + + private String mungeFontName (String name) { + // Roboto-ThinItalic => RobotoThin + // AndroidCock Regular => AndroidClock + String tails[] = { "Bold", "Italic", "Oblique", "Regular" }; + for (String tail : tails) { + String pres[] = { " ", "-", "_", "" }; + for (String pre : pres) { + int i = name.indexOf(pre + tail); + if (i > 0) name = name.substring (0, i); + } + } + return name; + } + + + private void scanSystemFonts() { + + // First parse the system font directories for the global fonts. + + String[] fontdirs = { "/system/fonts", "/system/font", "/data/fonts" }; + TTFAnalyzer analyzer = new TTFAnalyzer(); + for (String fontdir : fontdirs) { + File dir = new File(fontdir); + if (!dir.exists()) + continue; + File[] files = dir.listFiles(); + if (files == null) + continue; + + for (File file : files) { + String name = analyzer.getTtfFontName (file.getAbsolutePath()); + if (name == null) { + // LOG ("unparsable system font: %s", file); + } else { + name = mungeFontName (name); + if (! all_fonts.contains (name)) { + // LOG ("system font \"%s\" %s", name, file); + all_fonts.put (name, name); + } + } + } + } + + // Now parse our assets, for our bundled fonts. + + AssetManager am = app.getAssets(); + String dir = "fonts"; + String[] files = null; + try { files = am.list(dir); } + catch (Exception e) { LOG("listing assets: %s", e.toString()); } + + for (String fn : files) { + String fn2 = dir + "/" + fn; + Typeface t = Typeface.createFromAsset (am, fn2); + + File tmpfile = null; + try { + tmpfile = new File(app.getCacheDir(), fn); + if (tmpfile.createNewFile() == false) { + tmpfile.delete(); + tmpfile.createNewFile(); + } + + InputStream in = am.open (fn2); + FileOutputStream out = new FileOutputStream (tmpfile); + byte[] buffer = new byte[1024 * 512]; + while (in.read(buffer, 0, 1024 * 512) != -1) { + out.write(buffer); + } + out.close(); + in.close(); + + String name = analyzer.getTtfFontName (tmpfile.getAbsolutePath()); + tmpfile.delete(); + + name = mungeFontName (name); + all_fonts.put (name, t); + // LOG ("asset font \"%s\" %s", name, fn); + } catch (Exception e) { + if (tmpfile != null) tmpfile.delete(); + LOG ("error: %s", e.toString()); + } + } + } + + + // Parses family names from X Logical Font Descriptions, including a few + // standard X font names that aren't handled by try_xlfd_font(). + // Returns [ String name, Typeface ] + private Object[] parseXLFD (int mask, int traits, + String name, int name_type) { + boolean fixed = false; + boolean serif = false; + + int style_jwxyz = mask & traits; + + if (name_type != FONT_RANDOM) { + if ((style_jwxyz & STYLE_BOLD) != 0 || + name.equals("fixed") || + name.equals("courier") || + name.equals("console") || + name.equals("lucidatypewriter") || + name.equals("monospace")) { + fixed = true; + } else if (name.equals("times") || + name.equals("georgia") || + name.equals("serif")) { + serif = true; + } else if (name.equals("serif-monospace")) { + fixed = true; + serif = true; + } + } else { + Random r = new Random(); + serif = r.nextBoolean(); // Not much to randomize here... + fixed = (r.nextInt(8) == 0); + } + + name = (fixed + ? (serif ? "serif-monospace" : "monospace") + : (serif ? "serif" : "sans-serif")); + + int style_android = 0; + if ((style_jwxyz & STYLE_BOLD) != 0) + style_android |= Typeface.BOLD; + if ((style_jwxyz & STYLE_ITALIC) != 0) + style_android |= Typeface.ITALIC; + + return new Object[] { name, Typeface.create(name, style_android) }; + } + + + // Parses "Native Font Name One 12, Native Font Name Two 14". + // Returns [ String name, Typeface ] + private Object[] parseNativeFont (String name) { + Object font2 = all_fonts.get (name); + if (font2 instanceof String) + font2 = Typeface.create (name, Typeface.NORMAL); + return new Object[] { name, (Typeface)font2 }; + } + + + // Returns [ Paint paint, String family_name, Float ascent, Float descent ] + public Object[] loadFont(int mask, int traits, String name, int name_type, + float size) { + Object pair[]; + + if (name_type != FONT_RANDOM && name.equals("")) return null; + + if (name_type == FONT_FACE) { + pair = parseNativeFont (name); + } else { + pair = parseXLFD (mask, traits, name, name_type); + } + + String name2 = (String) pair[0]; + Typeface font = (Typeface) pair[1]; + + size *= 2; + + String suffix = (font.isBold() && font.isItalic() ? " bold italic" : + font.isBold() ? " bold" : + font.isItalic() ? " italic" : + ""); + Paint paint = new Paint(); + paint.setTypeface (font); + paint.setTextSize (size); + paint.setColor (Color.argb (0xFF, 0xFF, 0xFF, 0xFF)); + + LOG ("load font \"%s\" = \"%s %.1f\"", name, name2 + suffix, size); + + FontMetrics fm = paint.getFontMetrics(); + return new Object[] { paint, name2, -fm.ascent, fm.descent }; + } + + + /* Returns a byte[] array containing XCharStruct with an optional + bitmap appended to it. + lbearing, rbearing, width, ascent, descent: 2 bytes each. + Followed by a WxH pixmap, 32 bits per pixel. + */ + public ByteBuffer renderText (Paint paint, String text, boolean render_p, + boolean antialias_p) { + + if (paint == null) { + LOG ("no font"); + return null; + } + + /* Font metric terminology, as used by X11: + + "lbearing" is the distance from the logical origin to the leftmost + pixel. If a character's ink extends to the left of the origin, it is + negative. + + "rbearing" is the distance from the logical origin to the rightmost + pixel. + + "descent" is the distance from the logical origin to the bottommost + pixel. For characters with descenders, it is positive. For + superscripts, it is negative. + + "ascent" is the distance from the logical origin to the topmost pixel. + It is the number of pixels above the baseline. + + "width" is the distance from the logical origin to the position where + the logical origin of the next character should be placed. + + If "rbearing" is greater than "width", then this character overlaps the + following character. If smaller, then there is trailing blank space. + + The bbox coordinates returned by getTextBounds grow down and right: + for a character with ink both above and below the baseline, top is + negative and bottom is positive. + */ + paint.setAntiAlias (antialias_p); + FontMetrics fm = paint.getFontMetrics(); + Rect bbox = new Rect(); + paint.getTextBounds (text, 0, text.length(), bbox); + + /* The bbox returned by getTextBounds measures from the logical origin + with right and down being positive. This means most characters have + a negative top, and characters with descenders have a positive bottom. + */ + int lbearing = bbox.left; + int rbearing = bbox.right; + int ascent = -bbox.top; + int descent = bbox.bottom; + int width = (int) paint.measureText (text); + + int w = rbearing - lbearing; + int h = ascent + descent; + int size = 5 * 2 + (render_p ? w * h * 4 : 0); + + ByteBuffer bits = ByteBuffer.allocateDirect (size); + + bits.put ((byte) ((lbearing >> 8) & 0xFF)); + bits.put ((byte) ( lbearing & 0xFF)); + bits.put ((byte) ((rbearing >> 8) & 0xFF)); + bits.put ((byte) ( rbearing & 0xFF)); + bits.put ((byte) ((width >> 8) & 0xFF)); + bits.put ((byte) ( width & 0xFF)); + bits.put ((byte) ((ascent >> 8) & 0xFF)); + bits.put ((byte) ( ascent & 0xFF)); + bits.put ((byte) ((descent >> 8) & 0xFF)); + bits.put ((byte) ( descent & 0xFF)); + + if (render_p && w > 0 && h > 0) { + Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas (bitmap); + canvas.drawText (text, -lbearing, ascent, paint); + bitmap.copyPixelsToBuffer (bits); + bitmap.recycle(); + } + + return bits; + } + + + /* Returns the contents of the URL. + Loads the URL in a background thread: if the URL has not yet loaded, + this will return null. Once the URL has completely loaded, the full + contents will be returned. Calling this again after that starts the + URL loading again. + */ + private String loading_url = null; + private ByteBuffer loaded_url_body = null; + + public synchronized ByteBuffer loadURL (String url) { + + if (loaded_url_body != null) { // Thread finished + + // LOG ("textclient finished %s", loading_url); + + ByteBuffer bb = loaded_url_body; + loading_url = null; + loaded_url_body = null; + return bb; + + } else if (loading_url != null) { // Waiting on thread + // LOG ("textclient waiting..."); + return null; + + } else { // Launch thread + + loading_url = url; + LOG ("textclient launching %s...", url); + + new Thread (new Runnable() { + public void run() { + int size0 = 10240; + int size = size0; + int count = 0; + ByteBuffer body = ByteBuffer.allocateDirect (size); + + try { + URL u = new URL (loading_url); + // LOG ("textclient thread loading: %s", u.toString()); + InputStream s = u.openStream(); + byte buf[] = new byte[10240]; + while (true) { + int n = s.read (buf); + if (n == -1) break; + // LOG ("textclient thread read %d", n); + if (count + n + 1 >= size) { + int size2 = (int) (size * 1.2 + size0); + // LOG ("textclient thread expand %d -> %d", size, size2); + ByteBuffer body2 = ByteBuffer.allocateDirect (size2); + body.rewind(); + body2.put (body); + body2.position (count); + body = body2; + size = size2; + } + body.put (buf, 0, n); + count += n; + } + } catch (Exception e) { + LOG ("load URL error: %s", e.toString()); + body.clear(); + body.put (e.toString().getBytes()); + body.put ((byte) 0); + } + + // LOG ("textclient thread finished %s (%d)", loading_url, size); + loaded_url_body = body; + } + }).start(); + + return null; + } + } + + + // Returns [ Bitmap bitmap, String name ] + private Object[] convertBitmap (String name, Bitmap bitmap, + int target_width, int target_height, + ExifInterface exif, boolean rotate_p) { + if (bitmap == null) return null; + + { + + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + Matrix matrix = new Matrix(); + + LOG ("read image %s: %d x %d", name, width, height); + + // First rotate the image as per EXIF. + + if (exif != null) { + int deg = 0; + switch (exif.getAttributeInt (ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL)) { + case ExifInterface.ORIENTATION_ROTATE_90: deg = 90; break; + case ExifInterface.ORIENTATION_ROTATE_180: deg = 180; break; + case ExifInterface.ORIENTATION_ROTATE_270: deg = 270; break; + } + if (deg != 0) { + LOG ("%s: EXIF rotate %d", name, deg); + matrix.preRotate (deg); + if (deg == 90 || deg == 270) { + int temp = width; + width = height; + height = temp; + } + } + } + + // If the caller requested that we rotate the image to best fit the + // screen, rotate it again. + + if (rotate_p && + (width > height) != (target_width > target_height)) { + LOG ("%s: rotated to fit screen", name); + matrix.preRotate (90); + + int temp = width; + width = height; + height = temp; + } + + // Resize the image to be not larger than the screen, potentially + // copying it for the third time. + // Actually, always scale it, scaling up if necessary. + +// if (width > target_width || height > target_height) + { + float r1 = target_width / (float) width; + float r2 = target_height / (float) height; + float r = (r1 > r2 ? r2 : r1); + LOG ("%s: resize %.1f: %d x %d => %d x %d", name, + r, width, height, (int) (width * r), (int) (height * r)); + matrix.preScale (r, r); + } + + bitmap = Bitmap.createBitmap (bitmap, 0, 0, + bitmap.getWidth(), bitmap.getHeight(), + matrix, true); + + if (bitmap.getConfig() != Bitmap.Config.ARGB_8888) + bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false); + + return new Object[] { bitmap, name }; + + } + } + + + boolean havePermission(String permission) { + + if (Build.VERSION.SDK_INT < 16) { + return true; + } + + if (permissionGranted(permission)) { + return true; + } + + return false; + } + + + private boolean permissionGranted(String permission) { + boolean check = ContextCompat.checkSelfPermission(app, permission) == + PackageManager.PERMISSION_GRANTED; + return check; + } + + public Object[] checkThenLoadRandomImage (int target_width, int target_height, + boolean rotate_p) { + // RES introduced in API 16 + String permission = Manifest.permission.READ_EXTERNAL_STORAGE; + + if (havePermission(permission)) { + return loadRandomImage(target_width,target_height,rotate_p); + } else { + return null; + } + } + + public Object[] loadRandomImage (int target_width, int target_height, + boolean rotate_p) { + + int min_size = 480; + int max_size = 0x7FFF; + + ArrayList<String> imgs = new ArrayList<String>(); + + ContentResolver cr = app.getContentResolver(); + String[] cols = { MediaColumns.DATA, + MediaColumns.MIME_TYPE, + MediaColumns.WIDTH, + MediaColumns.HEIGHT }; + Uri uris[] = { + android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, + android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI }; + + for (int i = 0; i < uris.length; i++) { + Cursor cursor = cr.query (uris[i], cols, null, null, null); + if (cursor == null) + continue; + int j = 0; + int path_col = cursor.getColumnIndexOrThrow (cols[j++]); + int type_col = cursor.getColumnIndexOrThrow (cols[j++]); + int width_col = cursor.getColumnIndexOrThrow (cols[j++]); + int height_col = cursor.getColumnIndexOrThrow (cols[j++]); + while (cursor.moveToNext()) { + String path = cursor.getString(path_col); + String type = cursor.getString(type_col); + if (path != null && type != null && type.startsWith("image/")) { + String wc = cursor.getString(width_col); + String hc = cursor.getString(height_col); + if (wc != null && hc != null) { + int w = Integer.parseInt (wc); + int h = Integer.parseInt (hc); + if (w > min_size && h > min_size && + w < max_size && h < max_size) { + imgs.add (path); + } + } + } + } + cursor.close(); + } + + String which = null; + + int count = imgs.size(); + if (count == 0) { + LOG ("no images"); + return null; + } + + int i = new Random().nextInt (count); + which = imgs.get (i); + LOG ("picked image %d of %d: %s", i, count, which); + + Uri uri = Uri.fromFile (new File (which)); + String name = uri.getLastPathSegment(); + Bitmap bitmap = null; + ExifInterface exif = null; + + try { + try { + bitmap = MediaStore.Images.Media.getBitmap (cr, uri); + } catch (Exception e) { + LOG ("image %s unloadable: %s", which, e.toString()); + return null; + } + + try { + exif = new ExifInterface (uri.getPath()); // If it fails, who cares + } catch (Exception e) { + } + + return convertBitmap (name, bitmap, target_width, target_height, + exif, rotate_p); + } catch (java.lang.OutOfMemoryError e) { + LOG ("image %s got OutOfMemoryError: %s", which, e.toString()); + return null; + } + } + + + public Object[] getScreenshot (int target_width, int target_height, + boolean rotate_p) { + return convertBitmap ("Screenshot", screenshot, + target_width, target_height, + null, rotate_p); + } + + + public Bitmap decodePNG (byte[] data) { + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inPreferredConfig = Bitmap.Config.ARGB_8888; + return BitmapFactory.decodeByteArray (data, 0, data.length, opts); + } + + + // Sadly duplicated from jwxyz.h (and thence X.h and keysymdef.h) + // + private static final int ShiftMask = (1<<0); + private static final int LockMask = (1<<1); + private static final int ControlMask = (1<<2); + private static final int Mod1Mask = (1<<3); + private static final int Mod2Mask = (1<<4); + private static final int Mod3Mask = (1<<5); + private static final int Mod4Mask = (1<<6); + private static final int Mod5Mask = (1<<7); + private static final int Button1Mask = (1<<8); + private static final int Button2Mask = (1<<9); + private static final int Button3Mask = (1<<10); + private static final int Button4Mask = (1<<11); + private static final int Button5Mask = (1<<12); + + private static final int XK_Shift_L = 0xFFE1; + private static final int XK_Shift_R = 0xFFE2; + private static final int XK_Control_L = 0xFFE3; + private static final int XK_Control_R = 0xFFE4; + private static final int XK_Caps_Lock = 0xFFE5; + private static final int XK_Shift_Lock = 0xFFE6; + private static final int XK_Meta_L = 0xFFE7; + private static final int XK_Meta_R = 0xFFE8; + private static final int XK_Alt_L = 0xFFE9; + private static final int XK_Alt_R = 0xFFEA; + private static final int XK_Super_L = 0xFFEB; + private static final int XK_Super_R = 0xFFEC; + private static final int XK_Hyper_L = 0xFFED; + private static final int XK_Hyper_R = 0xFFEE; + + private static final int XK_Home = 0xFF50; + private static final int XK_Left = 0xFF51; + private static final int XK_Up = 0xFF52; + private static final int XK_Right = 0xFF53; + private static final int XK_Down = 0xFF54; + private static final int XK_Prior = 0xFF55; + private static final int XK_Page_Up = 0xFF55; + private static final int XK_Next = 0xFF56; + private static final int XK_Page_Down = 0xFF56; + private static final int XK_End = 0xFF57; + private static final int XK_Begin = 0xFF58; + + private static final int XK_F1 = 0xFFBE; + private static final int XK_F2 = 0xFFBF; + private static final int XK_F3 = 0xFFC0; + private static final int XK_F4 = 0xFFC1; + private static final int XK_F5 = 0xFFC2; + private static final int XK_F6 = 0xFFC3; + private static final int XK_F7 = 0xFFC4; + private static final int XK_F8 = 0xFFC5; + private static final int XK_F9 = 0xFFC6; + private static final int XK_F10 = 0xFFC7; + private static final int XK_F11 = 0xFFC8; + private static final int XK_F12 = 0xFFC9; + + public void sendKeyEvent (KeyEvent event) { + int uc = event.getUnicodeChar(); + int jcode = event.getKeyCode(); + int jmods = event.getModifiers(); + int xcode = 0; + int xmods = 0; + + switch (jcode) { + case KeyEvent.KEYCODE_SHIFT_LEFT: xcode = XK_Shift_L; break; + case KeyEvent.KEYCODE_SHIFT_RIGHT: xcode = XK_Shift_R; break; + case KeyEvent.KEYCODE_CTRL_LEFT: xcode = XK_Control_L; break; + case KeyEvent.KEYCODE_CTRL_RIGHT: xcode = XK_Control_R; break; + case KeyEvent.KEYCODE_CAPS_LOCK: xcode = XK_Caps_Lock; break; + case KeyEvent.KEYCODE_META_LEFT: xcode = XK_Meta_L; break; + case KeyEvent.KEYCODE_META_RIGHT: xcode = XK_Meta_R; break; + case KeyEvent.KEYCODE_ALT_LEFT: xcode = XK_Alt_L; break; + case KeyEvent.KEYCODE_ALT_RIGHT: xcode = XK_Alt_R; break; + + case KeyEvent.KEYCODE_HOME: xcode = XK_Home; break; + case KeyEvent.KEYCODE_DPAD_LEFT: xcode = XK_Left; break; + case KeyEvent.KEYCODE_DPAD_UP: xcode = XK_Up; break; + case KeyEvent.KEYCODE_DPAD_RIGHT: xcode = XK_Right; break; + case KeyEvent.KEYCODE_DPAD_DOWN: xcode = XK_Down; break; + //case KeyEvent.KEYCODE_NAVIGATE_PREVIOUS: xcode = XK_Prior; break; + case KeyEvent.KEYCODE_PAGE_UP: xcode = XK_Page_Up; break; + //case KeyEvent.KEYCODE_NAVIGATE_NEXT: xcode = XK_Next; break; + case KeyEvent.KEYCODE_PAGE_DOWN: xcode = XK_Page_Down; break; + case KeyEvent.KEYCODE_MOVE_END: xcode = XK_End; break; + case KeyEvent.KEYCODE_MOVE_HOME: xcode = XK_Begin; break; + + case KeyEvent.KEYCODE_F1: xcode = XK_F1; break; + case KeyEvent.KEYCODE_F2: xcode = XK_F2; break; + case KeyEvent.KEYCODE_F3: xcode = XK_F3; break; + case KeyEvent.KEYCODE_F4: xcode = XK_F4; break; + case KeyEvent.KEYCODE_F5: xcode = XK_F5; break; + case KeyEvent.KEYCODE_F6: xcode = XK_F6; break; + case KeyEvent.KEYCODE_F7: xcode = XK_F7; break; + case KeyEvent.KEYCODE_F8: xcode = XK_F8; break; + case KeyEvent.KEYCODE_F9: xcode = XK_F9; break; + case KeyEvent.KEYCODE_F10: xcode = XK_F10; break; + case KeyEvent.KEYCODE_F11: xcode = XK_F11; break; + case KeyEvent.KEYCODE_F12: xcode = XK_F12; break; + default: xcode = uc; break; + } + + if (0 != (jmods & KeyEvent.META_SHIFT_ON)) xmods |= ShiftMask; + if (0 != (jmods & KeyEvent.META_CAPS_LOCK_ON)) xmods |= LockMask; + if (0 != (jmods & KeyEvent.META_CTRL_MASK)) xmods |= ControlMask; + if (0 != (jmods & KeyEvent.META_ALT_MASK)) xmods |= Mod1Mask; + if (0 != (jmods & KeyEvent.META_META_ON)) xmods |= Mod1Mask; + if (0 != (jmods & KeyEvent.META_SYM_ON)) xmods |= Mod2Mask; + if (0 != (jmods & KeyEvent.META_FUNCTION_ON)) xmods |= Mod3Mask; + + /* If you touch and release Shift, you get no events. + If you type Shift-A, you get Shift down, A down, A up, Shift up. + So let's just ignore all lone modifier key events. + */ + if (xcode >= XK_Shift_L && xcode <= XK_Hyper_R) + return; + + boolean down_p = event.getAction() == KeyEvent.ACTION_DOWN; + sendKeyEvent (down_p, xcode, xmods); + } + + void start () { + if (render == null) { + animating_p = true; + render = new Thread(new Runnable() { + @Override + public void run() + { + int currentWidth, currentHeight; + synchronized (render) { + while (true) { + while (!animating_p || width == 0 || height == 0) { + try { + render.wait(); + } catch(InterruptedException exc) { + return; + } + } + + try { + nativeInit (hack, defaults, width, height, surface); + currentWidth = width; + currentHeight= height; + break; + } catch (SurfaceLost exc) { + width = 0; + height = 0; + } + } + } + + main_loop: + while (true) { + synchronized (render) { + assert width != 0; + assert height != 0; + while (!animating_p) { + try { + render.wait(); + } catch(InterruptedException exc) { + break main_loop; + } + } + + if (currentWidth != width || currentHeight != height) { + currentWidth = width; + currentHeight = height; + nativeResize (width, height, 0); + } + } + + long delay = nativeRender(); + + synchronized (render) { + if (delay != 0) { + try { + render.wait(delay / 1000, (int)(delay % 1000) * 1000); + } catch (InterruptedException exc) { + break main_loop; + } + } else { + if (Thread.interrupted ()) { + break main_loop; + } + } + } + } + + assert nativeRunningHackPtr != 0; + nativeDone (); + } + }); + + render.start(); + } else { + synchronized(render) { + animating_p = true; + render.notify(); + } + } + } + + void pause () { + if (render == null) + return; + synchronized (render) { + animating_p = false; + render.notify(); + } + } + + void close () { + if (render == null) + return; + synchronized (render) { + animating_p = false; + render.interrupt(); + } + try { + render.join(); + } catch (InterruptedException exc) { + } + render = null; + } + + void resize (int w, int h) { + assert w != 0; + assert h != 0; + if (render != null) { + synchronized (render) { + width = w; + height = h; + render.notify(); + } + } else { + width = w; + height = h; + } + } + + + /* We distinguish between taps and drags. + + - Drags/pans (down, motion, up) are sent to the saver to handle. + - Single-taps exit the saver. + - Long-press single-taps are sent to the saver as ButtonPress/Release; + - Double-taps are sent to the saver as a "Space" keypress. + + #### TODO: + - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow. + */ + + @Override + public boolean onSingleTapConfirmed (MotionEvent event) { + if (on_quit != null) + on_quit.run(); + return true; + } + + @Override + public boolean onDoubleTap (MotionEvent event) { + sendKeyEvent (new KeyEvent (KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_SPACE)); + return true; + } + + @Override + public void onLongPress (MotionEvent event) { + if (! button_down_p) { + int x = (int) event.getX (event.getPointerId (0)); + int y = (int) event.getY (event.getPointerId (0)); + sendButtonEvent (x, y, true); + sendButtonEvent (x, y, false); + } + } + + @Override + public void onShowPress (MotionEvent event) { + if (! button_down_p) { + button_down_p = true; + int x = (int) event.getX (event.getPointerId (0)); + int y = (int) event.getY (event.getPointerId (0)); + sendButtonEvent (x, y, true); + } + } + + @Override + public boolean onScroll (MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { + // LOG ("onScroll: %d", button_down_p ? 1 : 0); + if (button_down_p) + sendMotionEvent ((int) e2.getX (e2.getPointerId (0)), + (int) e2.getY (e2.getPointerId (0))); + return true; + } + + // If you drag too fast, you get a single onFling event instead of a + // succession of onScroll events. I can't figure out how to disable it. + @Override + public boolean onFling (MotionEvent e1, MotionEvent e2, + float velocityX, float velocityY) { + return false; + } + + public boolean dragEnded (MotionEvent event) { + if (button_down_p) { + int x = (int) event.getX (event.getPointerId (0)); + int y = (int) event.getY (event.getPointerId (0)); + sendButtonEvent (x, y, false); + button_down_p = false; + } + return true; + } + + @Override + public boolean onDown (MotionEvent event) { + return false; + } + + @Override + public boolean onSingleTapUp (MotionEvent event) { + return false; + } + + @Override + public boolean onDoubleTapEvent (MotionEvent event) { + return false; + } + + + static { + System.loadLibrary ("xscreensaver"); + +/* + Thread.setDefaultUncaughtExceptionHandler( + new Thread.UncaughtExceptionHandler() { + Thread.UncaughtExceptionHandler old_handler = + Thread.currentThread().getUncaughtExceptionHandler(); + + @Override + public void uncaughtException (Thread thread, Throwable ex) { + String err = ex.toString(); + Log.d ("xscreensaver", "Caught exception: " + err); + old_handler.uncaughtException (thread, ex); + } + }); +*/ + } +} |