/* * Copyright (C) 2011 Virginia Tech Department of Computer Science * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package sofia.app.internal; import sofia.app.ActivityStarter; import sofia.app.OptionsMenu; import sofia.app.Screen; import sofia.app.ScreenLayout; import sofia.internal.ModalTask; import sofia.internal.events.EventDispatcher; import sofia.internal.events.OptionalEventDispatcher; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.widget.ScrollView; import java.lang.ref.WeakReference; import java.lang.reflect.Method; import java.util.HashMap; import java.util.WeakHashMap; // ------------------------------------------------------------------------- /** * <p> * <em>This class is not intended for public use.</em> * </p><p> * This class provides the actual implementations of methods in the * {@link Screen} class, which "mixes in" this class through composition and * delegating methods. These methods are factored out here so that they can be * shared between {@link Screen} and its subclasses as well as * {@link MapScreen}, which cannot be a subclass of {@link Screen} because it * must extend {@code MapActivity} instead. * </p> * * @author Tony Allevato */ public class ScreenMixin { //~ Instance/static variables ............................................. public static final String SCREEN_ARGUMENTS = "sofia.app.internal.ScreenMixin.arguments"; public static final String SCREEN_RESULT = "sofia.app.internal.ScreenMixin.result"; private static final String INSTANCE_DATA = "sofia.app.internal.ScreenMixin.instanceData"; private static HashMap<Long, WeakReference<Object[]>> screenArguments = new HashMap<Long, WeakReference<Object[]>>(); private static HashMap<Long, Object> screenResults = new HashMap<Long, Object>(); private static HashMap<Long, AbsActivityStarter> startedActivities = new HashMap<Long, AbsActivityStarter>(); public static final int ACTIVITY_STARTER_REQUEST_CODE = 0x50F1A001; private Activity activity; private Bundle instanceData; private WeakHashMap<LifecycleInjection, Void> injections; private EventDispatcher initialize = new EventDispatcher("initialize"); //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Initializes a new {@code ScreenInternals} object. * * @param activity the activity that this object is associated with */ public ScreenMixin(Activity activity) { this.activity = activity; this.instanceData = new Bundle(); this.injections = new WeakHashMap<LifecycleInjection, Void>(); } //~ Methods ............................................................... // ---------------------------------------------------------- public static ScreenMixin getMixin(Context context) { try { Method method = context.getClass().getMethod("getScreenMixin"); return (ScreenMixin) method.invoke(context); } catch (Exception e) { return null; } } // ---------------------------------------------------------- public static boolean tryToAddLifecycleInjection( Context context, LifecycleInjection injection) { ScreenMixin mixin = getMixin(context); if (mixin != null) { mixin.addLifecycleInjection(injection); return true; } else { return false; } } // ---------------------------------------------------------- public void addLifecycleInjection(LifecycleInjection injection) { if (!injections.containsKey(injection)) { injections.put(injection, null); } } // ---------------------------------------------------------- public void removeLifecycleInjection(LifecycleInjection injection) { injections.remove(injection); } // ---------------------------------------------------------- public void runPauseInjections() { for (LifecycleInjection injection : injections.keySet()) { injection.pause(); } } // ---------------------------------------------------------- public void runResumeInjections() { for (LifecycleInjection injection : injections.keySet()) { injection.resume(); } } // ---------------------------------------------------------- public void runDestroyInjections() { for (LifecycleInjection injection : injections.keySet()) { injection.destroy(); } } // ---------------------------------------------------------- public Bundle getInstanceData() { return instanceData; } // ---------------------------------------------------------- public void saveInstanceState(Bundle bundle) { bundle.putBundle(INSTANCE_DATA, instanceData); } // ---------------------------------------------------------- public void restoreInstanceState(Bundle bundle) { if (bundle != null) { instanceData = bundle.getBundle(INSTANCE_DATA); } } // ---------------------------------------------------------- /** * Displays a confirmation dialog and waits for the user to select an * option. * * @param title the title to display in the dialog * @param message the message to display in the dialog * @return true if the user clicked the "Yes" option; false if the user * clicked the "No" option or cancelled the dialog (for example, by * pressing the Back button) */ public boolean showConfirmationDialog( final String title, final String message) { ModalTask<Boolean> modal = new ModalTask<Boolean>() { @Override protected void run() { AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(title); builder.setMessage(message); builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { endModal(true); } }); builder.setNegativeButton("No", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { endModal(false); } }); builder.setOnCancelListener( new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { endModal(false); } }); builder.show(); } }; return modal.executeTask(); } // ---------------------------------------------------------- /** * Displays an alert dialog and waits for the user to dismiss it. * * @param title the title to display in the dialog * @param message the message to display in the dialog */ public void showAlertDialog(final String title, final String message) { ModalTask<Void> modal = new ModalTask<Void>() { @Override protected void run() { AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(title); builder.setMessage(message); builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { endModal(null); } }); builder.setOnCancelListener( new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { endModal(null); } }); builder.show(); } }; modal.executeTask(); } // ---------------------------------------------------------- /** * Display a popup list to the user and waits for them to select an item * from it. Items in the list will be rendered simply by calling the * {@link Object#toString()} method. To control the item renderer used to * display the list, see * {@link #selectItemFromList(String, List, ItemRenderer)}. * * @param <Item> the type of items in the list, which is inferred from the * {@code list} parameter * @param title the title of the popup dialog * @param list the list of items to display in the popup * @param itemRenderer the item renderer to use to display each item * @return the item that was selected from the list, or null if the dialog * was cancelled */ /*public <Item> Item selectItemFromList( final String title, final List<? extends Item> list, final ItemRenderer itemRenderer) { ModalTask<Item> modal = new ModalTask<Item>() { @Override protected void run() { AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(title); AnnotatedAdapter adapter = new AnnotatedAdapter(activity, list, ItemRenderer.RenderingContext.DIALOG); builder.setAdapter(adapter, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { endModal(list.get(which)); } }); builder.setOnCancelListener( new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { endModal(null); } }); builder.show(); } }; return modal.executeTask(); }*/ // ---------------------------------------------------------- /** * Starts the activity with the specified intent. This method will not * return until the new activity is dismissed by the user. * * @param intent an {@code Intent} that describes the activity to start * @param callback the name of the method that should be called on the * previous activity when the new one returns */ public void presentActivity(Intent intent, String callback) { new ActivityStarter(intent, callback).start(activity); } // ---------------------------------------------------------- /** * Starts the activity represented by the specified {@code Screen} subclass * and slides it into view. This method will not return until the new * screen is dismissed by the user. * * @param <ResultType> * @param screenClass the subclass of {@code Screen} that will be displayed * @param resultClass * @param args the arguments to be sent to the screen's {@code initialize} * method * @return */ public void presentScreen( Class<? extends Activity> screenClass, Object... args) { new ActivityStarter(screenClass, args).start(activity); } // ---------------------------------------------------------- /** * Call this method when the current screen is finished and should be * closed. The specified value will be passed back to the previous screen * and returned from the {@link #presentScreen(Class, Class, Object...)} * call that originally presented this screen. * * @param result the value to pass back to the previous screen */ public void finish(Object result) { Intent intent = new Intent(); intent.putExtra(SCREEN_RESULT, registerScreenResult(result)); activity.setResult(Activity.RESULT_OK, intent); activity.finish(); } // ---------------------------------------------------------- public void startActivityForResult(AbsActivityStarter starter, Intent intent, int requestCode) { long timestamp = System.currentTimeMillis(); startedActivities.put(timestamp, starter); instanceData.putLong("startedActivity", timestamp); activity.startActivityForResult(intent, requestCode); } // ---------------------------------------------------------- /** * Called by a screen to handle an {@code onActivityResult} call that * originated from one of the activities above. * * @param requestCode * @param resultCode * @param data */ public void handleOnActivityResult(int requestCode, int resultCode, Intent data) { long activityCode = instanceData.getLong("startedActivity", 0); if (activityCode != 0) { AbsActivityStarter starter = startedActivities.remove(activityCode); starter.handleActivityResult( activity, data, requestCode, resultCode); } instanceData.remove("startedActivity"); } // ---------------------------------------------------------- public static long registerScreenArguments(Object... args) { long timestamp = System.currentTimeMillis(); screenArguments.put(timestamp, new WeakReference<Object[]>(args)); return timestamp; } // ---------------------------------------------------------- public Object[] getScreenArguments(Intent intent) { WeakReference<Object[]> ref = null; if (intent != null) { long timestamp = intent.getLongExtra(SCREEN_ARGUMENTS, 0); ref = screenArguments.get(timestamp); } Object[] args = null; if (ref != null) { args = ref.get(); } if (args == null) { args = new Object[0]; } return args; } // ---------------------------------------------------------- public static long registerScreenResult(Object result) { long timestamp = System.currentTimeMillis(); screenResults.put(timestamp, result); return timestamp; } // ---------------------------------------------------------- public static Object takeScreenResult(Intent intent) { if (intent != null) { long timestamp = intent.getLongExtra(SCREEN_RESULT, 0); return screenResults.remove(timestamp); } else { return null; } } // ---------------------------------------------------------- /** * Search for and inflate the screen's layout, if possible. Returns true if * a layout was found or false if it was not. * * @return true if a layout was found, otherwise false */ public boolean tryToInflateLayout() { // Check for a @ScreenLayout annotation on the Screen subclass and // inflate the view if so; otherwise, do a smart search for a resource // based on the class name. If none is found it is assumed that the // user will call setContentView directly in initialize. ScreenLayout screenLayout = activity.getClass().getAnnotation(ScreenLayout.class); int id = ResourceResolver.resolve(activity, screenLayout); if (id != 0) { if (screenLayout != null && screenLayout.scroll()) { LayoutInflater inflater = activity.getLayoutInflater(); ScrollView scroller = new ScrollView(activity); inflater.inflate(id, scroller); activity.setContentView(scroller); } else { activity.setContentView(id); } return true; } else { return false; } } // ---------------------------------------------------------- /** * Invokes the {@code initialize} method on the activity that matches the * specified argument list, if there is one. * * @param args the arguments to pass to {@code initialize} */ public void invokeInitialize(Object[] args) { // Load any persistent data saved from a previous instance of the // activity. PersistenceManager.getInstance().loadPersistentContext(activity); // Dynamically call the initialize method that takes the specified // arguments, or throw an exception if none exists. if (initialize.isSupportedBy(activity, args)) { initialize.dispatch(activity, args); } else { StringBuffer argClasses = new StringBuffer(); for (int i = 0; i < args.length; i++) { argClasses.append(args[i].getClass().getSimpleName()); if (i < args.length - 1) { argClasses.append(", "); } } throw new IllegalStateException( activity.getClass().getSimpleName() + " does not have a " + "method initialize(" + argClasses.toString() + ") or " + "one with compatible arguments."); } } // ---------------------------------------------------------- public boolean onCreateOptionsMenu(Menu menu) { OptionsMenu menuAnnotation = activity.getClass().getAnnotation(OptionsMenu.class); if (menuAnnotation != null) { int id = ResourceResolver.resolve(activity, menuAnnotation); MenuInflater inflater = activity.getMenuInflater(); inflater.inflate(id, menu); return true; } else { return false; } } // ---------------------------------------------------------- public boolean onMenuItemSelected(int featureId, MenuItem item) { String id = activity.getResources().getResourceEntryName( item.getItemId()); boolean called = false; if (id != null) { OptionalEventDispatcher event = new OptionalEventDispatcher(id + "Clicked"); called = event.dispatch(activity, item); } return called; } }