/*******************************************************************************
* Copyright 2011 The Regents of the University of California
*
* 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 org.ohmage.prompt.remoteactivity;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.ohmage.R;
import org.ohmage.logprobe.Log;
import org.ohmage.prompt.AbstractPrompt;
import java.util.Iterator;
import java.util.List;
/**
* Prompt that will launch a remote Activity that can either be part of this
* application or another application installed on the system. The remote
* Activity can be called as soon as this prompt loads by setting the
* 'autolaunch' field or later via a "Replay" button. The number of replays
* can also be set.
*
* @author John Jenkins
* @version 1.0
*/
public class RemoteActivityPrompt extends AbstractPrompt implements OnClickListener
{
private static final String TAG = "RemoteActivityPrompt";
private static final String FEEDBACK_STRING = "feedback";
private static final String SINGLE_VALUE_STRING = "score";
private String packageName;
private String activityName;
private String actionName;
private String input;
private JSONArray responseArray;
private TextView feedbackText;
private Button launchButton;
private Activity callingActivity;
private boolean launched;
private boolean autolaunch;
private int runs;
private int minRuns;
private int retries;
private String mFeedback;
/**
* Basic default constructor.
*/
public RemoteActivityPrompt()
{
super();
launched = false;
runs = 0;
}
/**
* Creates the View from an XML file and sets up the local variables for
* the Views contained within. Then, if automatic launch is turned on it
* will attempt to automatically launch the remote Activity.
*/
@Override
public View getView(Context context)
{
try
{
callingActivity = (Activity) context;
}
catch(ClassCastException e)
{
callingActivity = null;
Log.e(TAG, "getView() recieved a Context that wasn't an Activity.", e);
// Should we error out here?
}
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.prompt_remote_activity, null);
feedbackText = (TextView) layout.findViewById(R.id.prompt_remote_activity_feedback);
feedbackText.setText(mFeedback);
launchButton = (Button) layout.findViewById(R.id.prompt_remote_activity_replay_button);
launchButton.setOnClickListener(this);
launchButton.setText((!launched && !autolaunch) ? R.string.prompt_remote_launch : R.string.prompt_remote_relaunch);
if(retries > 0)
{
launchButton.setVisibility(View.VISIBLE);
}
else
{
if(!launched && !autolaunch)
{
launchButton.setVisibility(View.VISIBLE);
}
else
{
launchButton.setVisibility(View.GONE);
}
}
if(autolaunch && !launched)
{
launchActivity();
}
return layout;
}
/**
* If the 'resultCode' indicates failure then we treat it as if the user
* has skipped the prompt. If skipping is not allowed, we log it as an
* error, but we do not make an entry in the results array to prevent
* corrupting it nor do we set as skipped to prevent us from corrupting
* the entire survey.
*
* If the 'resultCode' indicates success then we check to see what was
* returned via the parameterized 'data' object. If 'data' is null, we put
* an empty JSONObject in the array to indicate that something went wrong.
* If 'data' is not null, we get all the key-value pairs from the data's
* extras and place them in a JSONObject. If the keys for these extras are
* certain "special" return codes, some of which are required, then we
* handle those as well which may or may not include putting them in the
* JSONObject. Finally, we put the JSONObject in the JSONArray that is the
* return value for this prompt type.
*/
@Override
public void handleActivityResult(Context context, int resultCode, Intent data)
{
if(resultCode == Activity.RESULT_CANCELED)
{
if(mSkippable.equalsIgnoreCase("true"))
{
this.setSkipped(true);
}
else if(mSkippable.equalsIgnoreCase("false"))
{
// The Activity was canceled for some reason, but it shouldn't
// have been.
Log.e(TAG, "The Activity was canceled, but the prompt isn't set as skippable.");
}
else
{
// This should _never_ happen!
Log.e(TAG, "Invalid 'skippable' value: " + mSkippable);
}
}
else if(resultCode == Activity.RESULT_OK)
{
if(data != null)
{
if(responseArray == null)
{
responseArray = new JSONArray();
}
boolean singleValueFound = false;
JSONObject currResponse = new JSONObject();
Bundle extras = data.getExtras();
Iterator<String> keysIter = extras.keySet().iterator();
while(keysIter.hasNext())
{
String nextKey = keysIter.next();
if(FEEDBACK_STRING.equals(nextKey))
{
mFeedback = extras.getString(nextKey);
feedbackText.setText(mFeedback);
}
else
{
try
{
currResponse.put(nextKey, extras.get(nextKey));
}
catch(JSONException e)
{
Log.e(TAG, "Invalid return value from remote Activity for key: " + nextKey);
}
if(SINGLE_VALUE_STRING.equals(nextKey))
{
singleValueFound = true;
}
}
}
if(singleValueFound)
{
responseArray.put(currResponse);
}
else
{
// We cannot add this to the list of responses because it
// will be rejected for not containing the single-value
// value.
Log.e(TAG, "The remote Activity is not returning a single value which is required for CSV export.");
}
}
else
{
// If the data is null, we put an empty JSONObject in the
// array to indicate that the data was null.
responseArray.put(new JSONObject());
Log.e(TAG, "The data returned by the remote Activity was null.");
}
}
// TODO: Possibly support user-defined Activity results:
// resultCode > Activity.RESULT_FIRST_USER
//
// One obvious possibility is some sort of "SKIPPED" return code.
}
/**
* Returns true if the number of runs is greater than the minimum required
* number of runs.
*/
@Override
public boolean isPromptAnswered() {
return(runs >= minRuns);
}
/**
* Returns the JSONObject that it has created from the values Bundled in
* the return of the remote Activity if the remote Activity has been run
* at least the minimum number of times. If not, it returns null as an
* indicator that the prompt isn't sufficiently "answered".
*/
@Override
protected Object getTypeSpecificResponseObject()
{
return responseArray;
}
/**
* There are no extras for this object.
*/
@Override
protected Object getTypeSpecificExtrasObject()
{
return null;
}
/**
* The text to be displayed to the user if the prompt is considered
* unanswered.
*/
@Override
public String getUnansweredPromptText() {
return("Please launch the remote Activity at least " + (minRuns - runs) + " more times.");
}
/**
* Clears the local variable by recreating and reinstantiating it to a new
* one.
*/
@Override
protected void clearTypeSpecificResponseData()
{
responseArray = new JSONArray();
}
/**
* Called when the "Replay" button is clicked. If autolaunch is not on and
* the remote Activity hasn't been launched yet, it will switch the text
* back to "Replay" from "Play" and launch the Activity. If there weren't
* any replays allowed, it will also remove the "Replay" button.
*
* If the remote Activity has been launched be it from this button or from
* the autolaunch, it will check the number of retries left. If it is out
* of retries then it is an error to be in this state, but it will simply
* hide the button and leave the function. If there are retries left, it
* will decrement the number left, check if the user is out of replays in
* which case it will hide the "Replay" button, and will launch the remote
* Activity.
*/
@Override
public void onClick(View v)
{
if(launched)
{
if((retries + 1 - runs) > 0)
{
launchActivity();
if((retries + 1 - runs) <= 0)
{
launchButton.setVisibility(View.GONE);
}
}
else
{
launchButton.setVisibility(View.GONE);
}
}
else if(!autolaunch)
{
launchButton.setText(R.string.prompt_remote_relaunch);
if((retries + 1 - runs) <= 0)
{
launchButton.setVisibility(View.GONE);
}
launchActivity();
}
else
{
Log.e(TAG, "Autolaunch is turned on, but I received a click on the \"Replay\" button before ever launching the remote Activity.");
launchActivity();
}
}
/**
* Sets the name of the Package to which the remote Activity belongs.
*
* @param packageName The name of the Package to which the remote Activity
* belongs.
*
* @throws IllegalArgumentException Thrown if the 'packageName' is null or
* an empty string.
*
* @see {@link #setActivity(String)}
* @see {@link #setAction(String)}
*/
public void setPackage(String packageName) throws IllegalArgumentException
{
if((packageName == null) || packageName.equals(""))
{
throw new IllegalArgumentException("Invalid Package name.");
}
this.packageName = packageName;
}
/**
* Sets the Activity to be called within the remote Package.
*
* @param activityName The name of the Activity to be called within the
* remote Package.
*
* @throws IllegalArgumentException Thrown if 'activityName' is null or an
* empty string.
*
* @see {@link #setPackage(String)}
* @see {@link #setAction(String)}
*/
public void setActivity(String activityName) throws IllegalArgumentException
{
if((activityName == null) || packageName.equals(""))
{
throw new IllegalArgumentException("Invalid Activity name.");
}
this.activityName = activityName;
}
/**
* Sets the name of the Action as it is defined in the intent-filter of
* its Activity definition in the Manifest of its remote application.
*
* @param activityName The String representing the Action to be set in the
* Intent to the remote Activity.
*
* @throws IllegalArgumentException Thrown if 'actionName' is "null" or
* an empty string.
*
* @see {@link #setPackage(String)}
* @see {@link #setActivity(String)}
*/
public void setAction(String actionName) throws IllegalArgumentException
{
if((actionName == null) || (actionName.equals("")))
{
throw new IllegalArgumentException("Invalid Action name.");
}
this.actionName = actionName;
}
/**
* Sets the number of times that a user can relaunch the remote Activity.
*
* @param numRetries The number of times a user can relaunch the remote
* Activity.
*/
public void setRetries(int numRetries)
{
retries = numRetries;
}
/**
* Returns the number of remaining retries.
*
* @return The number of times the remote Activity can be relaunched via
* the "Replay" button, not counting the first replay if
* 'autolaunch' is off.
*/
public int getNumRetriesRemaining()
{
return retries;
}
/**
* Sets the minimum number of times the remote Activity must be run in
* order to consider this prompt "answered".
*
* @param minRuns The minimum number of times the remote Activity must be
* launched.
*/
public void setMinRuns(int minRuns)
{
this.minRuns = minRuns;
}
/**
* Sets whether or not the Activity will launch automatically when the
* prompt is displayed. If it is not set to automatically launch then the
* "Replay" button will be shown and have its text set to "Play". On
* subsequent displays of this Activity, the button will have its text
* switched back to "Replay".
*
* @param autolaunch Whether or not the remote Activity should be launched
* automatically when this view loads.
*/
public void setAutolaunch(boolean autolaunch)
{
this.autolaunch = autolaunch;
}
/**
* Returns whether or not the remote Activity has ever been launched from
* this prompt. This includes both the 'autolaunch' and the "Replay"
* button.
*
* @return Whether or not the remote Activity has ever been launched from
* this prompt.
*/
public boolean hasLaunchedRemoteActivity()
{
return launched;
}
/**
* Sets the input for the remote Activity that is being called.
*
* @param input The input to be passed into the remote Activity when it is
* launched.
*/
public void setInput(String input)
{
this.input = input;
}
/**
* Creates an Intent from the given 'activityName' and then launches the
* Intent.
*/
private void launchActivity()
{
if(callingActivity != null)
{
Intent activityToLaunch = new Intent(actionName);
activityToLaunch.setComponent(new ComponentName(packageName, activityName));
activityToLaunch.putExtra("input", input);
if(isCallable(activityToLaunch)) {
callingActivity.startActivityForResult(activityToLaunch, REQUEST_CODE);
launched = true;
runs++;
} else {
Toast.makeText(callingActivity, "Required component is not installed", Toast.LENGTH_SHORT).show();
}
}
else
{
Log.e(TAG, "The calling Activity was null.");
}
}
private boolean isCallable(Intent intent) {
List<ResolveInfo> list = callingActivity.getPackageManager().queryIntentActivities(intent, 0);
return list.size() > 0;
}
}