Java tutorial
/******************************************************************************* * Cyril M. Hansen 2013 * * Licences : * Creative Commons Attribution-ShareAlike 3.0 * Creative Commons Attribution - Partage dans les Mmes Conditions 3.0 France * * http://creativecommons.org/licenses/by-sa/3.0 * http://creativecommons.org/licenses/by-sa/3.0/fr/ * * Sources : * https://github.com/cyrilmhansen/DIYGlslLwp ******************************************************************************/ package com.softwaresemantics.diyglsllwp; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.nio.ByteBuffer; import android.annotation.SuppressLint; import android.app.Activity; import android.app.ProgressDialog; import android.app.WallpaperManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewConfiguration; import android.view.Window; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.Button; import android.widget.LinearLayout; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; import com.badlogic.gdx.backends.android.CustomAndroidGDXApp; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.Pixmap.Format; /** * Main activity / view * * * TODO : improve data model management, add simple cache between gallery page * resize image to smaller size to improve memory footprint * */ public class ShaderGalleryActivity extends CustomAndroidGDXApp implements ScreenshotProcessor, ClickHandler, NativeCallback { private static final String HTTP_GLSL_HEROKU_COM_ITEM = "http://glslsandbox.com/item/"; private static final String COM_SOFTWARESEMANTICS_DIYGLSLLWP_PREFS_LWP = "com.softwaresemantics.diyglsllwp.LivewallpaperSettings"; static final String DIY_GLSL_LWP_DIR_NAME = "DiyGlslLwp"; private static final String COM_HEROKU_GLSL = "com.glslsandbox"; private static final int _200_PX = 200; // TODO : dynamic layout private static final int FS_BUTTON_HEIGHT = 150; private static final int REQUEST_SET_LIVE_WALLPAPER = 101; public static String CURRENT_FRAG_SHADER_PROGRAM = "CURRENT_FRAG_SHADER_PROGRAM"; public static String CURRENT_VERT_SHADER_PROGRAM = "CURRENT_VERT_SHADER_PROGRAM"; private String currentFragShaderProgram = null; private Entry[] values; private ShaderEntryArrayAdapter adapter; // Gallery page numbering as used by glsl.heroku.com (starts at 0) private int currentPageIndex = -1; private int askedPageIndex = 0; private LinearLayout glayout; private View rootMainView; private int currentSelectedIndex = -1; DIYGslSurface mySurface; private String screenShotFilename; private int nbElementParPage; // Preference access LiveWallpaperPrefs prefs; // "GL" view private View glslView; // To be displayed during download tasks private ProgressDialog progressDialog; public void onWindowFocusChanged(boolean visible) { checkIfOnlineOrExit(); } private void checkIfOnlineOrExit() { if (!isOnline()) { Toast.makeText(this, getResources().getString(R.string.networkRequired), Toast.LENGTH_LONG).show(); exit(); } } @Override protected void onResume() { super.onResume(); checkIfOnlineOrExit(); if (askedPageIndex != currentPageIndex) { // gallery page needs to be downloaded updateGallery(askedPageIndex); } else { if (currentSelectedIndex >= 0) { // restore current selection mList.setSelection(currentSelectedIndex); } } } @SuppressLint("NewApi") public void onCreate(Bundle icicle) { super.onCreate(icicle); LiveWallpaperService.galleryAppInstance = this; // Create a new Handler that is being associated to the // main thread. handler = new Handler(); prefs = new LiveWallpaperPrefs(this); // extract shader from intent if any Bundle extras = this.getIntent().getExtras(); if (extras != null) { currentFragShaderProgram = extras.getString(CURRENT_FRAG_SHADER_PROGRAM); } // Option menu must be accessible, either by key or action bar // API level 14 try { if (!ViewConfiguration.get(this).hasPermanentMenuKey()) { // Ensure action bar is visible getWindow().requestFeature(Window.FEATURE_ACTION_BAR); // getActionBar().hide(); } else { requestWindowFeature(Window.FEATURE_NO_TITLE); } } catch (Error err) { // API < 14 ( => java.lang.NoSuchMethodError) // Option key is required on this system requestWindowFeature(Window.FEATURE_NO_TITLE); } // Try to to keep the UI fluid android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); nbElementParPage = 50; values = new Entry[nbElementParPage]; for (int i = 0; i < nbElementParPage; i++) { values[i] = new Entry("#" + i); } adapter = new ShaderEntryArrayAdapter(this, values); setListAdapter(adapter); // TODO even more declarative layout glayout = new LinearLayout(this); glayout.setOrientation(LinearLayout.VERTICAL); // Inflate main layout LayoutInflater inflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE); rootMainView = inflater.inflate(R.layout.main, null, false); createGDXPreview(currentFragShaderProgram); glayout.addView(rootMainView); setContentView(glayout); findViewById(R.id.nextPageButton).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO What if we are at the last page ?? // FIXME Theoric CRASH askedPageIndex = currentPageIndex + 1; cancelSelectionIfAny(); updateGallery(); } }); findViewById(R.id.prevPageButton).setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (currentPageIndex > 0) { askedPageIndex = currentPageIndex - 1; cancelSelectionIfAny(); updateGallery(); } } }); progressDialog = new ProgressDialog(this); updateStatusLabel(getResources().getString(R.string.loadingStatus)); setupButtons(); } public void updateStatusLabel(String alternateLabel) { TextView statusTextView = (TextView) findViewById(R.id.statusLabel); if (statusTextView == null) { Log.e("diyglsllwp", "updateStatusLabel statusLabel elt missing"); } if (alternateLabel != null) { statusTextView.setText(alternateLabel); } else { statusTextView.setText("http://glsl.heroku.com/?page=" + currentPageIndex); } } /** * Setup of app menu */ public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.option, menu); return true; } public boolean onOptionsItemSelected(MenuItem item) { // Menu action dispatch switch (item.getItemId()) { case R.id.goToShader: DialogUtils.inputDialog(this, getResources().getString(R.string.inputShaderRef), false, new InputDialogCallback() { @Override public void inputValue(String value) { try { new InternetAsyncShaderTask(ShaderGalleryActivity.this, 0) .execute(HTTP_GLSL_HEROKU_COM_ITEM + value); } catch (Exception ignored) { } } }); return true; case R.id.goToPage: DialogUtils.inputDialog(this, getResources().getString(R.string.inputGalleryPage), true, new InputDialogCallback() { @Override public void inputValue(String value) { try { askedPageIndex = Integer.valueOf(value); currentSelectedIndex = -1; updateGallery(askedPageIndex); } catch (Exception ignored) { } } }); return true; case R.id.prefsLWP: startPrefsLwpActivity(); return true; case R.id.aPropos: showAproposDialog(); return true; case R.id.quit: Toast.makeText(ShaderGalleryActivity.this, getResources().getString(R.string.exiting), Toast.LENGTH_SHORT).show(); moveTaskToBack(true); return true; } return false; } private void setupButtons() { // All buttons are hidden initially Button buttonViewFS = (Button) findViewById(R.id.buttonViewFS); Button buttonDownload = (Button) findViewById(R.id.buttonDownload); Button buttonEdit = (Button) findViewById(R.id.buttonEdit); // Setup click handler unconditionally buttonViewFS.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { setupFullScreenView(); } }); buttonEdit.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(ShaderGalleryActivity.this, "Launching Editor", Toast.LENGTH_SHORT).show(); openCurrentSelectedShaderInSystemEditor(); } }); buttonDownload.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(ShaderGalleryActivity.this, "Processing Save request", Toast.LENGTH_SHORT).show(); saveCurrentSelectedShader(); } }); } private void showAproposDialog() { AboutDialog about = new AboutDialog(this); about.setTitle(getResources().getString(R.string.aPropos)); about.show(); } private void startPrefsLwpActivity() { Intent intent = new Intent(this, LivewallpaperSettings.class); intent.setAction(COM_SOFTWARESEMANTICS_DIYGLSLLWP_PREFS_LWP); startActivityForResult(intent, REQUEST_SET_LIVE_WALLPAPER); } private void updateGallery() { updateGallery(askedPageIndex); } private void updateGallery(int pageIndex) { if (!isOnline()) { Toast.makeText(this, getResources().getString(R.string.networkRequired), Toast.LENGTH_LONG).show(); // call finish(); ?? } else { progressDialog.setMessage(getResources().getString(R.string.loadingGalleryPage) + pageIndex); progressDialog.show(); // defered loading of gallery new InternetAsyncGalleryTask(this, pageIndex).execute("http://glsl.heroku.com/?page=" + pageIndex); } } protected void onListItemClick(ListView l, View v, int position, long id) { Entry item = (Entry) getListAdapter().getItem(position); // hidePreviousSelectionButton(); progressDialog.setMessage(getResources().getString(R.string.loadingShader) + item.getRefId()); progressDialog.show(); // shader download task that will call us back to update UI new InternetAsyncShaderTask(this, position).execute(HTTP_GLSL_HEROKU_COM_ITEM + item.getRefId()); } /** * Do what we can to help the user active the builtin Live Wallpaper Code * from stackoverflow... on non standard ROMS, this will fail */ @SuppressLint("InlinedApi") public void setOwnLWP() { Intent intent; // try the new Jelly Bean direct android wallpaper chooser first try { ComponentName component = new ComponentName(LiveWallpaperService.class.getPackage().getName(), LiveWallpaperService.class.getCanonicalName()); intent = new Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER); intent.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, component); startActivityForResult(intent, REQUEST_SET_LIVE_WALLPAPER); } catch (android.content.ActivityNotFoundException e3) { // try the generic android wallpaper chooser next try { intent = new Intent(WallpaperManager.ACTION_LIVE_WALLPAPER_CHOOSER); startActivityForResult(intent, REQUEST_SET_LIVE_WALLPAPER); Toast.makeText(ShaderGalleryActivity.this, getResources().getString(R.string.manualSelectLWP), Toast.LENGTH_LONG).show(); } catch (android.content.ActivityNotFoundException e2) { // that failed, let's try the nook intent try { intent = new Intent(); intent.setAction("com.bn.nook.CHANGE_WALLPAPER"); startActivity(intent); } catch (android.content.ActivityNotFoundException e) { // everything failed, let's notify the user Toast.makeText(ShaderGalleryActivity.this, getResources().getString(R.string.errorLaunchingLWPSelector), Toast.LENGTH_SHORT) .show(); } } } } public boolean isOnline() { ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo netInfo = cm.getActiveNetworkInfo(); if (netInfo != null && netInfo.isConnectedOrConnecting()) { return true; } return false; } public Entry[] getValues() { return values; } public void setValues(Entry[] values) { this.values = values; } public void galleryUpdated(int askedPageIndex) { adapter.notifyDataSetChanged(); currentPageIndex = askedPageIndex; updateStatusLabel(null); progressDialog.dismiss(); } protected ListAdapter mAdapter; protected ListView mList; private Handler mHandler = new Handler(); private boolean mFinishedStart = false; private Runnable mRequestFocus = new Runnable() { public void run() { mList.focusableViewAvailable(mList); } }; /** * Ensures the list view has been created before Activity restores all of * the view states. * * @see Activity#onRestoreInstanceState(Bundle) */ @Override protected void onRestoreInstanceState(Bundle state) { ensureList(); super.onRestoreInstanceState(state); } /** * @see Activity#onDestroy() */ @Override protected void onDestroy() { mHandler.removeCallbacks(mRequestFocus); LiveWallpaperService.galleryAppInstance = null; super.onDestroy(); } @Override protected void onPause() { progressDialog.dismiss(); super.onPause(); // Force complete restart of gallery each time exit(); } /** * Updates the screen state (current list and other views) when the content * changes. * * @see Activity#onContentChanged() */ @Override public void onContentChanged() { super.onContentChanged(); View emptyView = findViewById(android.R.id.empty); mList = (ListView) findViewById(android.R.id.list); if (mList == null) { throw new RuntimeException( "Your content must have a ListView whose id attribute is " + "'android.R.id.list'"); } if (emptyView != null) { mList.setEmptyView(emptyView); } mList.setOnItemClickListener(mOnClickListener); if (mFinishedStart) { setListAdapter(mAdapter); } mHandler.post(mRequestFocus); mFinishedStart = true; setSelection(currentSelectedIndex); } /** * Provide the cursor for the list view. */ public void setListAdapter(ListAdapter adapter) { synchronized (this) { ensureList(); mAdapter = adapter; mList.setAdapter(adapter); } } /** * Set the currently selected list item to the specified position with the * adapter's data * * @param position */ public void setSelection(int position) { mList.setSelection(position); } /** * Get the position of the currently selected list item. */ public int getSelectedItemPosition() { return mList.getSelectedItemPosition(); } /** * Get the cursor row ID of the currently selected list item. */ public long getSelectedItemId() { return mList.getSelectedItemId(); } /** * Get the activity's list view widget. */ public ListView getListView() { ensureList(); return mList; } /** * Get the ListAdapter associated with this activity's ListView. */ public ListAdapter getListAdapter() { return mAdapter; } private void ensureList() { if (mList != null) { return; } setContentView(android.R.layout.list_content); } private AdapterView.OnItemClickListener mOnClickListener = new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View v, int position, long id) { onListItemClick((ListView) parent, v, position, id); } }; private Button cancelButton; private Button setAsLWPButton; private AndroidApplicationConfiguration cfg; private Toast toast; public void runShaderinPreview(String code) { if (glslView != null) { // Remove previously created view glayout.removeView(glslView); } createGDXPreview(code); } private void createGDXPreview(String code) { cfg = new AndroidApplicationConfiguration(); cfg.useGL20 = true; cfg.resolutionStrategy = new PreviewResStrategy(this, _200_PX); // TODO : settings for preview DIYGslSurface.setRenderGuard(true); mySurface = new DIYGslSurface(code, true, 4, true, true, true, 4, true, 60, true, 1.0f); mySurface.setScreenshotProc(this); // impossible to check immediately for GL20 / Surface is created // asynchronously we had a callback to get a chance to notify the user mySurface.setNativeCallback(this); glslView = initializeForView(mySurface, cfg); DIYGslSurface.setRenderGuard(false); // Add callback for click in preview mode mySurface.addClickHandler(this); currentFragShaderProgram = code; glayout.addView(glslView, 0); } public void showButtonForSelection(int rowIndex) { cancelSelectionIfAny(); ((Entry) mList.getItemAtPosition(rowIndex)).setSelected(true); currentSelectedIndex = rowIndex; // method "copied" from ListActivity maybe to be refactored here setSelection(rowIndex); // Mode changes will be automatically handled by the UI onContentChanged(); } public File saveCurrentSelectedShader() { // FIXME assert String refId = "default"; if (currentSelectedIndex >= 0) { refId = values[currentSelectedIndex].getRefId(); } return saveCurrentSelectedShader(ShaderStorage.getDiyGlslLwpSubDir(this, COM_HEROKU_GLSL), refId); } public File saveCurrentSelectedShaderAsLWP() { File lwp = saveCurrentSelectedShader(ShaderStorage.getDiyGlslLwpShaderDir(this), ShaderStorage.LWP); LiveWallpaperService.reloadShaderIfNeeded(); return lwp; } public File saveCurrentSelectedShader(File targetDir, String prefix) { // fixme assert String shaderPrg = currentFragShaderProgram; File targetFile = null; try { // SD Ext targetDir.mkdirs(); targetFile = new File(targetDir, prefix + ShaderStorage.TXT); PrintStream ps = new PrintStream(targetFile); ps.print(shaderPrg); ps.close(); screenShotFilename = new File(targetDir, prefix + ShaderStorage.JPG).getAbsolutePath(); // Get callback during next redraw for screenshot mySurface.setDoscreenShotRequest(true); } catch (Exception ex) { Log.e("ShaderGalleryActivity", "saveCurrentShader", ex); } return targetFile; } void openCurrentSelectedShaderInSystemEditor() { // Save required before anything File tmpglsltxt = saveCurrentSelectedShader(); // Start external editor (system knows which one) Intent intent = new Intent(Intent.ACTION_VIEW); Uri uri = Uri.fromFile(tmpglsltxt); intent.setDataAndType(uri, "text/plain"); // TODO : try to improve encoding handling // TODO : Add live editing of fragment shader startActivity(intent); } private void cancelSelectionIfAny() { // don't trust current selection as it may be corrupted by threading / // delay issues in tasks for (Entry value : values) { value.setSelected(false); } currentSelectedIndex = 0; setSelection(currentSelectedIndex); } public void saveScreenshot() { int height = glslView.getHeight(); int width = glslView.getWidth(); Pixmap pixmap = getScreenshot(0, 0, width, height, true); // PixmapIO.writePNG(file, pixmap); // too inefficient saveAsJpeg(new FileHandle(screenShotFilename), pixmap); } /** * * * @param jpgfile * @param pixmap */ public void saveAsJpeg(FileHandle jpgfile, Pixmap pixmap) { FileOutputStream fos; int x = 0, y = 0; int xl = 0, yl = 0; try { Bitmap bmp = Bitmap.createBitmap(pixmap.getWidth(), pixmap.getHeight(), Bitmap.Config.ARGB_8888); // we need to switch between LibGDX RGBA format to Android ARGB // format for (x = 0, xl = pixmap.getWidth(); x < xl; x++) { for (y = 0, yl = pixmap.getHeight(); y < yl; y++) { int color = pixmap.getPixel(x, y); // RGBA => ARGB int RGB = color >> 8; int A = (color & 0x000000ff) << 24; int ARGB = A | RGB; bmp.setPixel(x, y, ARGB); } } // Finished Color format conversion fos = new FileOutputStream(jpgfile.file()); bmp.compress(CompressFormat.JPEG, 90, fos); // Finished Comression to JPEG file fos.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } } /** * * */ public Pixmap getScreenshot(int x, int y, int w, int h, boolean flipY) { Gdx.gl20.glPixelStorei(GL20.GL_PACK_ALIGNMENT, 1); final Pixmap pixmap = new Pixmap(w, h, Format.RGBA8888); ByteBuffer pixels = pixmap.getPixels(); Gdx.gl20.glReadPixels(x, y, w, h, GL20.GL_RGBA, GL20.GL_UNSIGNED_BYTE, pixels); final int numBytes = w * h * 4; byte[] lines = new byte[numBytes]; if (flipY) { final int numBytesPerLine = w * 4; for (int i = 0; i < h; i++) { pixels.position((h - i - 1) * numBytesPerLine); pixels.get(lines, i * numBytesPerLine, numBytesPerLine); } pixels.clear(); pixels.put(lines); } else { pixels.clear(); pixels.get(lines); } return pixmap; } /** * setupFullScreenView */ public void setupFullScreenView() { if (glslView != null) { // Remove previously created view glayout.removeView(glslView); } cfg = new AndroidApplicationConfiguration(); cfg.useGL20 = true; // TODO more dynamic layout for this screen cfg.resolutionStrategy = new AlmostFSResStrategy(this, (int) (2 * FS_BUTTON_HEIGHT * getResources().getDisplayMetrics().density)); mySurface = new DIYGslSurface(currentFragShaderProgram, prefs.isReductionFactorEnabled(), prefs.getReductionFactor(), true, true, prefs.isTimeDithering(), prefs.getTimeDitheringFactor(), prefs.getTimeLoopPeriod() != null, prefs.getTimeLoopPeriod() != null ? prefs.getTimeLoopPeriod() : 60, prefs.isForceMediumP(), prefs.getSpeedFactor()); glslView = initializeForView(mySurface, cfg); cancelButton = new Button(this); cancelButton.setHeight(FS_BUTTON_HEIGHT); cancelButton.setText(getResources().getString(R.string.cancel)); setAsLWPButton = new Button(this); setAsLWPButton.setHeight(FS_BUTTON_HEIGHT); setAsLWPButton.setText(getResources().getString(R.string.setAsLWP)); setAsLWPButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { ShaderGalleryActivity.this.setAsLWP(); backToMainView(); } }); cancelButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { backToMainView(); } }); glayout.removeView(rootMainView); glayout.addView(setAsLWPButton); glayout.addView(cancelButton); glayout.addView(glslView, 0); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); } private void backToMainView() { glayout.removeView(setAsLWPButton); glayout.removeView(cancelButton); glayout.removeView(glslView); createGDXPreview(currentFragShaderProgram); glayout.addView(rootMainView, 1); getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); } protected void setAsLWP() { saveCurrentSelectedShaderAsLWP(); setOwnLWP(); } @Override public void doProcessScreenShot() { saveScreenshot(); screenShotFilename = null; } public void hideProgressDialog() { progressDialog.dismiss(); } @Override protected void onRestart() { super.onRestart(); } @Override protected void onStart() { super.onStart(); } @Override protected void onStop() { super.onStop(); } @Override public void onClick(int x, int y) { runOnUiThread(new UIFullViewRunnable(this)); } private void goToFullViewIfPossible() { if (currentFragShaderProgram != null) { setupFullScreenView(); } } class UIFullViewRunnable implements Runnable { private ShaderGalleryActivity parentActivity; public UIFullViewRunnable(ShaderGalleryActivity activity) { this.parentActivity = activity; } public void run() { this.parentActivity.goToFullViewIfPossible(); ; } } @Override public void onRequirementFailure(String msg) { Toast.makeText(this, getResources().getString(R.string.openGLRequirementsFailure), Toast.LENGTH_LONG) .show(); exit(); } @Override public void onResumeGDX() { // do nothing in Gallery } @Override public void notifyCompilationEnd() { runOnUiThread(new Runnable() { @Override public void run() { if (toast != null) { toast.cancel(); } } }); } @Override public void notifyCompilation() { runOnUiThread(new Runnable() { @Override public void run() { toast = Toast.makeText(ShaderGalleryActivity.this, getResources().getString(R.string.processingShader), Toast.LENGTH_LONG); toast.show(); } }); } }