Java tutorial
/* @license * This file is part of the Game Closure SDK. * * The Game Closure SDK is free software: you can redistribute it and/or modify * it under the terms of the Mozilla Public License v. 2.0 as published by Mozilla. * The Game Closure SDK is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * Mozilla Public License v. 2.0 for more details. * You should have received a copy of the Mozilla Public License v. 2.0 * along with the Game Closure SDK. If not, see <http://mozilla.org/MPL/2.0/>. */ package com.tealeaf; import java.io.InputStream; import java.io.FileOutputStream; import java.io.File; import android.content.pm.ActivityInfo; import com.tealeaf.ActivityState; import com.tealeaf.event.BackButtonEvent; import com.tealeaf.event.JSUpdateNotificationEvent; import com.tealeaf.event.KeyboardScreenResizeEvent; import com.tealeaf.event.LaunchTypeEvent; import com.tealeaf.event.MarketUpdateNotificationEvent; import com.tealeaf.event.OnUpdatedEvent; import com.tealeaf.event.PauseEvent; import com.tealeaf.event.PhotoBeginLoadedEvent; import com.tealeaf.event.WindowFocusAcquiredEvent; import com.tealeaf.event.WindowFocusLostEvent; import com.tealeaf.plugin.PluginManager; import com.tealeaf.util.ILogger; import android.graphics.Rect; import android.view.ViewTreeObserver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.content.SharedPreferences.Editor; import android.content.SharedPreferences; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.graphics.Paint; import android.graphics.Matrix; import android.graphics.Point; import android.media.AudioManager; import android.media.ExifInterface; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.provider.MediaStore.MediaColumns; import android.provider.MediaStore; import android.support.v4.app.FragmentActivity; import android.view.Display; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup.LayoutParams; import android.view.Window; import android.view.WindowManager; import android.widget.AbsoluteLayout; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.RelativeLayout; import org.json.JSONObject; /* * FIXME general things * there's /a lot/ of stuff in this class. Much of it needs to be moved into * other places so that each unit only has one concern. * * Longer term, this activity should become the activity that always starts and * then figures out which game activity to run. */ public class TeaLeaf extends FragmentActivity { private TeaLeafOptions options; public TeaLeafGLSurfaceView glView; private boolean glViewPaused; private boolean isFullScreen; protected FrameLayout group; protected Overlay overlay; protected TextInputView textboxview; private TextEditViewHandler textEditView; private Uri launchURI; private int lastVisibleHeight = -1; private ContactList contactList; private SoundQueue soundQueue; protected LocalStorage localStorage; private ResourceManager resourceManager; private Settings settings; private IMenuButtonHandler menuButtonHandler; private BroadcastReceiver screenOffReciever; private ILogger remoteLogger; public boolean takeScreenshot = false; private static TeaLeaf instance = null; public static TeaLeaf get() { return instance; } private boolean paused = true; //variables for pause and resume boolean didPause = true; boolean didResume = false; boolean didLoseFocus = true; boolean didRegainFocus = false; //edit text public EditTextView editText; /** * * @return true if the app is running and in foreground and false if it's * either not running or in the background. */ public static boolean inForeground() { TeaLeaf tealeaf = TeaLeaf.get(); boolean inForeground = false; if (tealeaf != null) { inForeground = !tealeaf.paused; } return inForeground; } public ILogger getLoggerInstance(Context context) { return new RemoteLogger(context); } public String getLaunchUri() { return launchURI.toString(); } public String getCodeHost() { return "http://" + options.getCodeHost() + ":" + options.getCodePort() + "/"; } public TeaLeafOptions getOptions() { return options; } public ILogger getRemoteLogger() { return remoteLogger; } public ContactList getContactList() { return contactList; } public SoundQueue getSoundQueue() { return soundQueue; } public Settings getSettings() { return settings; } public LocalStorage getLocalStorage() { return localStorage; } public ResourceManager getResourceManager() { return resourceManager; } // FIXME this shouldn't be necessary, but TeaLeafGLSurfaceView needs to know if there's a textbox layer public boolean hasTextInputView() { return textboxview != null; } // FIXME this shouldn't be necessary, but TeaLeafGLSurfaceView needs to know if there's an overlay public boolean hasOverlay() { return overlay != null; } public synchronized Overlay getOverlay() { if (overlay == null) { overlay = new Overlay(this); group.addView(overlay, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); overlay.bringToFront(); } return overlay; } public synchronized TextInputView getTextInputView() { if (textboxview == null) { textboxview = new TextInputView(TeaLeaf.this); group.addView(textboxview, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); if (overlay != null) { overlay.bringToFront(); } } return textboxview; } protected void moveViewsToFront() { // if the textbox view is available, it should be ontop of the gl view if (textboxview != null) { textboxview.bringToFront(); } // if the overlay is available, it should be the absolute topmost layer if (overlay != null) { overlay.bringToFront(); } } public void clearLocalStorage() { localStorage.clear(); } public void clearTextures() { if (glView != null) { glView.clearTextures(); } } public void restartGLView() { logger.log("{tealeaf} Restarting GL View"); if (glView != null) { glView.restart(); glViewPaused = false; } } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); if (getOptions().isDevelop()) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.debugmenu, menu); return true; } else { return false; } } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (!menuButtonHandler.onPress(id)) { return super.onOptionsItemSelected(item); } else { return true; } } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); PluginManager.init(this); instance = this; setFullscreenFlag(); configureActivity(); String appID = findAppID(); options = new TeaLeafOptions(this); PluginManager.callAll("onCreate", this, savedInstanceState); //check intent for test app info Bundle bundle = getIntent().getExtras(); boolean isTestApp = false; if (bundle != null) { isTestApp = bundle.getBoolean("isTestApp", false); if (isTestApp) { options.setAppID(appID); boolean isPortrait = bundle.getBoolean("isPortrait", false); if (isPortrait) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } options.setCodeHost(bundle.getString("hostValue")); options.setCodePort(bundle.getInt("portValue")); String simulateID = bundle.getString("simulateID"); options.setSimulateID(simulateID); } } getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); group = new FrameLayout(this); setContentView(group); // TextEditViewHandler setup textEditView = new TextEditViewHandler(this); settings = new Settings(this); remoteLogger = (ILogger) getLoggerInstance(this); checkUpdate(); compareVersions(); setLaunchUri(); // defer building all of these things until we have the absolutely correct options logger.buildLogger(this, remoteLogger); resourceManager = new ResourceManager(this, options); contactList = new ContactList(this, resourceManager); soundQueue = new SoundQueue(this, resourceManager); localStorage = new LocalStorage(this, options); // start push notifications, but defer for 10 seconds to give us time to start up PushBroadcastReceiver.scheduleNext(this, 10); glView = new TeaLeafGLSurfaceView(this); glViewPaused = false; // default screen dimensions Display display = getWindow().getWindowManager().getDefaultDisplay(); int width = display.getWidth(); int height = display.getHeight(); int orientation = getRequestedOrientation(); // gets real screen dimensions without nav bars on recent API versions if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { Point screenSize = new Point(); try { display.getRealSize(screenSize); width = screenSize.x; height = screenSize.y; } catch (NoSuchMethodError e) { } } // flip width and height based on orientation if ((orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE && height > width) || (orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT && width > height)) { int tempWidth = width; width = height; height = tempWidth; } final AbsoluteLayout absLayout = new AbsoluteLayout(this); absLayout.setLayoutParams(new android.view.ViewGroup.LayoutParams(width, height)); absLayout.addView(glView, new android.view.ViewGroup.LayoutParams(width, height)); group.addView(absLayout); editText = EditTextView.Init(this); if (isTestApp) { startGame(); } soundQueue.playSound(SoundQueue.LOADING_SOUND); doFirstRun(); remoteLogger.sendLaunchEvent(this); paused = false; menuButtonHandler = MenuButtonHandlerFactory.getButtonHandler(this); group.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { public void onGlobalLayout() { // get visible area of the view Rect r = new Rect(); group.getWindowVisibleDisplayFrame(r); int visibleHeight = r.bottom; // TODO // maybe this should be renamed if (visibleHeight != lastVisibleHeight) { lastVisibleHeight = visibleHeight; EventQueue.pushEvent(new KeyboardScreenResizeEvent(visibleHeight)); } } }); } public Bitmap getBitmapFromView(EditText view) { //Define a bitmap with the same size as the view Bitmap returnedBitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); //Bind a canvas to it Canvas canvas = new Canvas(returnedBitmap); //Get the view's background Drawable bgDrawable = view.getBackground(); if (bgDrawable != null) { //has background drawable, then draw it on the canvas bgDrawable.draw(canvas); } // draw the view on the canvas view.draw(canvas); Paint p = new Paint(); p.setColor(Color.BLACK); p.setTextSize(24); canvas.drawText("'ello mate", 0, 0, p); //return the bitmap return returnedBitmap; } public void pauseGL() { logger.log("{tealeaf} pauseGL called"); if (glView != null && !glViewPaused) { logger.log("{tealeaf} Pausing glView"); glView.onPause(); glViewPaused = true; } } public void resumeGL() { logger.log("{tealeaf} resumeGL called"); if (glView != null) { logger.log("{tealeaf} Resuming glView"); glView.queueResumeEvent(); glView.onResume(); glViewPaused = false; } } private void checkUpdate() { if (settings.isUpdateReady(options.getBuildIdentifier())) { if (settings.isMarketUpdate(options.getBuildIdentifier())) { // redirect the user to the market logger.log("{updates} Got a startup market update"); EventQueue.pushEvent(new MarketUpdateNotificationEvent()); } } } public void makeOverlay() { overlay = new Overlay(this); group.addView(overlay, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); overlay.bringToFront(); } public void setServer(String host, int port) { options.setCodeHost(host); options.setTcpHost(host); options.setCodePort(port); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); Editor editor = prefs.edit(); editor.putString("@__prev_host__", host); editor.putInt("@__prev_port__", port); editor.commit(); setLaunchUri(); } @Override protected void onStart() { super.onStart(); PluginManager.callAll("onStart"); } @Override public void onNewIntent(Intent intent) { PluginManager.callAll("onNewIntent", intent); } private void getLaunchType(Intent intent) { Uri data = intent.getData(); LaunchTypeEvent event; //launch type "notification" has been taken out if (data != null) { logger.log("{tealeaf} Launched with intent url:", data.toString()); event = new LaunchTypeEvent("url", data.toString()); } else { event = new LaunchTypeEvent("standard"); } EventQueue.pushEvent(event); } // This is called when focus and onResume have both been achieved private void onRealResume() { logger.log("{tealeaf} Activity has resumed"); if (glView != null) { logger.log("{tealeaf} Resuming GL"); resumeGL(); soundQueue.onResume(); soundQueue.playSound(SoundQueue.LOADING_SOUND); PluginManager.callAll("onResume"); } } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { logger.log("{focus} Gained focus"); ActivityState.onWindowFocusAcquired(); if (ActivityState.hasResumed(true)) { onRealResume(); } registerScreenOffReceiver(); // always send acquired focus event EventQueue.pushEvent(new WindowFocusAcquiredEvent()); // games are inherently full screen and immersive, hide OS UI bars if (isFullScreen && Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { int uiFlag = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { uiFlag |= View.SYSTEM_UI_FLAG_FULLSCREEN; uiFlag |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; uiFlag |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; uiFlag |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { uiFlag |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; } } getWindow().getDecorView().setSystemUiVisibility(uiFlag); } } else { logger.log("{focus} Lost focus"); ActivityState.onWindowFocusLost(); pause(); unregisterReceiver(screenOffReciever); if (jsRunning) { //always send lost focus event String[] events = { new WindowFocusLostEvent().pack() }; // DANGER: Calling dispatchEvents() is NOT thread-safe. // Doing it here because the GLThread is paused. NativeShim.dispatchEvents(events); } } } // Functions/variable used to indicate if javascript steps // should execute and if events should be dispatched private boolean jsRunning = true; public void stopJS() { jsRunning = false; } public void startJS() { jsRunning = true; } public boolean isJSRunning() { return jsRunning; } @Override protected void onPause() { logger.log("{tealeaf} Activity got onPause"); super.onPause(); ActivityState.onPause(); pauseGL(); paused = true; pause(); } private void pause() { if (ActivityState.hasPaused(true)) { //TODO only dispatch this event when JS is running. if (glView != null && glView.running()) { if (!glView.isResumeEventQueued()) { PluginManager.callAll("onPause"); if (jsRunning) { String[] events = { new PauseEvent().pack() }; // DANGER: Calling dispatchEvents() is NOT thread-safe. // Doing it here because the GLThread is paused. NativeShim.dispatchEvents(events); } glView.setRendererStateReloading(); } soundQueue.onPause(); soundQueue.pauseSound(SoundQueue.LOADING_SOUND); } } } public void onConfigurationChanged(Configuration config) { super.onConfigurationChanged(config); } @Override protected void onResume() { logger.log("{tealeaf} Resume"); super.onResume(); ActivityState.onResume(); paused = false; if (settings.isUpdateReady(options.getBuildIdentifier())) { if (settings.isMarketUpdate(options.getBuildIdentifier())) { // market update logger.log("{updates} Got a resume market update"); EventQueue.pushEvent(new MarketUpdateNotificationEvent()); } else { // js update logger.log("{updates} Got a resume JS update"); EventQueue.pushEvent(new JSUpdateNotificationEvent()); } } if (ActivityState.hasResumed(true)) { onRealResume(); } getLaunchType(getIntent()); } @Override protected void onStop() { super.onStop(); PluginManager.callAll("onStop"); } @Override public void onBackPressed() { String[] objs = PluginManager.callAll("consumeOnBackPressed"); boolean consume = true; for (String o : objs) { if (o != null && Boolean.valueOf(o)) { consume = true; break; } consume = false; } if (consume) { EventQueue.pushEvent(new BackButtonEvent()); } else { PluginManager.callAll("onBackPressed"); } } @Override public void onDestroy() { super.onDestroy(); PluginManager.callAll("onDestroy"); logger.log("{tealeaf} Destroy"); glView.destroy(); NativeShim.reset(); } private void setFullscreenFlag() { try { Bundle metaData = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA).metaData; isFullScreen = metaData.getBoolean("fullscreen", true); } catch (NameNotFoundException e) { logger.log(e); //Default to fullscreen mode. isFullScreen = true; } } private String findAppID() { String appid = getIntent().getStringExtra("appid"); if (appid != null) { return appid; } // FIXME HACK: find a better way to determine the appID try { Bundle metaData = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA).metaData; return metaData.containsKey("appID") ? metaData.get("appID").toString() : "tealeaf"; } catch (NameNotFoundException e) { logger.log(e); return "tealeaf"; } } private void configureActivity() { getWindow().requestFeature(Window.FEATURE_NO_TITLE); if (isFullScreen) { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } setVolumeControlStream(AudioManager.STREAM_MUSIC); } private void compareVersions() { String newVersion = options.getBuildIdentifier(); String oldVersion = settings.getString("version", null); boolean firstRun = oldVersion == null; if (!newVersion.equals(oldVersion)) { // new version, send an event and clean the old files EventQueue.pushEvent(new OnUpdatedEvent(oldVersion, newVersion, firstRun)); settings.setString("version", newVersion); } } private void doFirstRun() { if (settings.isFirstRun()) { remoteLogger.sendFirstLaunchEvent(this); settings.markFirstRun(); } } private void setLaunchUri() { launchURI = getIntent().getData(); if (launchURI == null) { launchURI = Uri.parse(getCodeHost() + options.getAppID() + "/"); } else { if (launchURI.isRelative()) { launchURI = Uri.parse("http:" + launchURI.toString()); } else { launchURI = Uri.parse(launchURI.toString().replace(launchURI.getScheme(), "http")); } } } public void startGame() { glView.start(); glView.setVisibility(View.VISIBLE); } protected void reset() { group.removeView(glView); glViewPaused = false; glView.destroy(); NativeShim.reset(); Intent intent = getIntent(); overridePendingTransition(0, 0); intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); finish(); overridePendingTransition(0, 0); startActivity(intent); } private void makeScreenOffReceiver() { screenOffReciever = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // TODO Auto-generated method stub soundQueue.onPause(); soundQueue.pauseSound(SoundQueue.LOADING_SOUND); } }; } private void registerScreenOffReceiver() { if (screenOffReciever == null) { makeScreenOffReceiver(); } IntentFilter filter = new IntentFilter(); filter.addAction("android.intent.action.SCREEN_OFF"); registerReceiver(screenOffReciever, filter); } static int ROTATE_0 = 0; static int ROTATE_90 = 90; static int ROTATE_180 = 180; static int ROTATE_270 = 270; private Bitmap rotateBitmap(Bitmap bitmap, int rotate) { // rotate as needed Bitmap bmp; //only allow the largest size to be 768 for now, several phones //including the galaxy s3 seem to crash with rotating very large //images (out of memory errors) from the gallery int w = bitmap.getWidth(); int h = bitmap.getHeight(); Bitmap scaled = bitmap; if (w > h && w > 768) { float ratio = 768.f / (float) w; w = 768; h = (int) (ratio * h); scaled = Bitmap.createScaledBitmap(bitmap, w, h, true); if (bitmap != scaled) { bitmap.recycle(); } } if (h > w && h > 768) { float ratio = 768.f / (float) h; h = 768; w = (int) (ratio * w); scaled = Bitmap.createScaledBitmap(bitmap, w, h, true); if (bitmap != scaled) { bitmap.recycle(); } } int newWidth = scaled.getWidth(); int newHeight = scaled.getHeight(); int degrees = 0; if (rotate == ROTATE_90 || rotate == ROTATE_270) { newWidth = scaled.getHeight(); newHeight = scaled.getWidth(); } Matrix matrix = new Matrix(); matrix.postRotate(rotate); bmp = Bitmap.createBitmap(scaled, 0, 0, scaled.getWidth(), scaled.getHeight(), matrix, true); if (scaled != bmp) { scaled.recycle(); } return bmp; } // TODO: can this be called after your activity is recycled, meaning we're never going to see these events? protected void onActivityResult(int request, int result, Intent data) { super.onActivityResult(request, result, data); PluginManager.callAll("onActivityResult", request, result, data); logger.log("GOT ACTIVITY RESULT WITH", request, result); switch (request) { case PhotoPicker.CAPTURE_IMAGE: if (result == RESULT_OK) { EventQueue.pushEvent(new PhotoBeginLoadedEvent()); Bitmap bmp = null; if (data != null) { Bundle extras = data.getExtras(); //try and get bitmap off of intent if (extras != null) { bmp = (Bitmap) extras.get("data"); } } //try the large file on disk final File f = PhotoPicker.getCaptureImageTmpFile(); if (f != null && f.exists()) { new Thread(new Runnable() { public void run() { Bitmap bmp = null; String filePath = f.getAbsolutePath(); try { bmp = BitmapFactory.decodeFile(filePath); } catch (OutOfMemoryError e) { System.gc(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 4; bmp = BitmapFactory.decodeFile(filePath, options); } if (bmp != null) { try { File f = new File(filePath); ExifInterface exif = new ExifInterface(f.getAbsolutePath()); int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); if (orientation != ExifInterface.ORIENTATION_NORMAL) { int rotateBy = 0; switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: rotateBy = ROTATE_90; break; case ExifInterface.ORIENTATION_ROTATE_180: rotateBy = ROTATE_180; break; case ExifInterface.ORIENTATION_ROTATE_270: rotateBy = ROTATE_270; break; } Bitmap rotatedBmp = rotateBitmap(bmp, rotateBy); if (rotatedBmp != bmp) { bmp.recycle(); } bmp = rotatedBmp; } } catch (Exception e) { logger.log(e); } f.delete(); if (bmp != null) { glView.getTextureLoader() .saveCameraPhoto(glView.getTextureLoader().getCurrentPhotoId(), bmp); glView.getTextureLoader().finishCameraPicture(); } else { glView.getTextureLoader().failedCameraPicture(); } } } }).start(); } else { glView.getTextureLoader().saveCameraPhoto(glView.getTextureLoader().getCurrentPhotoId(), bmp); glView.getTextureLoader().finishCameraPicture(); } } else { glView.getTextureLoader().failedCameraPicture(); } break; case PhotoPicker.PICK_IMAGE: if (result == RESULT_OK) { final Uri selectedImage = data.getData(); EventQueue.pushEvent(new PhotoBeginLoadedEvent()); String[] filePathColumn = { MediaColumns.DATA, MediaStore.Images.ImageColumns.ORIENTATION }; String _filepath = null; try { Cursor cursor = getContentResolver().query(selectedImage, filePathColumn, null, null, null); cursor.moveToFirst(); int columnIndex = cursor.getColumnIndex(filePathColumn[0]); _filepath = cursor.getString(columnIndex); columnIndex = cursor.getColumnIndex(filePathColumn[1]); int orientation = cursor.getInt(columnIndex); cursor.close(); } catch (Exception e) { } final String filePath = _filepath; new Thread(new Runnable() { public void run() { if (filePath == null) { BitmapFactory.Options options = new BitmapFactory.Options(); InputStream inputStream; Bitmap bmp = null; try { inputStream = getContentResolver().openInputStream(selectedImage); bmp = BitmapFactory.decodeStream(inputStream, null, options); inputStream.close(); } catch (Exception e) { logger.log(e); } if (bmp != null) { glView.getTextureLoader() .saveGalleryPicture(glView.getTextureLoader().getCurrentPhotoId(), bmp); glView.getTextureLoader().finishGalleryPicture(); } else { glView.getTextureLoader().failedGalleryPicture(); } } else { Bitmap bmp = null; try { bmp = BitmapFactory.decodeFile(filePath); } catch (OutOfMemoryError e) { System.gc(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 4; bmp = BitmapFactory.decodeFile(filePath, options); } if (bmp != null) { try { File f = new File(filePath); ExifInterface exif = new ExifInterface(f.getAbsolutePath()); int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); if (orientation != ExifInterface.ORIENTATION_NORMAL) { int rotateBy = 0; switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: rotateBy = ROTATE_90; break; case ExifInterface.ORIENTATION_ROTATE_180: rotateBy = ROTATE_180; break; case ExifInterface.ORIENTATION_ROTATE_270: rotateBy = ROTATE_270; break; } Bitmap rotatedBmp = rotateBitmap(bmp, rotateBy); if (rotatedBmp != bmp) { bmp.recycle(); } bmp = rotatedBmp; } } catch (Exception e) { logger.log(e); } if (bmp != null) { glView.getTextureLoader() .saveGalleryPicture(glView.getTextureLoader().getCurrentPhotoId(), bmp); glView.getTextureLoader().finishGalleryPicture(); } else { glView.getTextureLoader().failedGalleryPicture(); } } } } }).start(); } else { glView.getTextureLoader().failedGalleryPicture(); } break; } } public TextEditViewHandler getTextEditViewHandler() { return textEditView; } public FrameLayout getGroup() { return group; } public void reload() { } static { System.loadLibrary("tealeaf"); } @Override public void onLowMemory() { if (glView != null) { NativeShim.textureManagerMemoryCritical(); } } @Override public void onTrimMemory(int level) { if (glView != null) { glView.onMemoryWarning(level); } } }