/* Copyright 2012 Google Inc.
*
* 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 com.mobiperf;
import java.security.Security;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.TabActivity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.TabHost;
import android.widget.TextView;
import android.widget.Toast;
import com.mobiperf.MeasurementScheduler.SchedulerBinder;
/**
* The main UI thread that manages different tabs
*/
public class SpeedometerApp extends TabActivity {
public static final String TAG = "MobiPerf";
private boolean userConsented = false;
private String selectedAccount = null;
private static final int DIALOG_CONSENT = 0;
private static final int DIALOG_ACCOUNT_SELECTOR = 1;
private MeasurementScheduler scheduler;
private TabHost tabHost;
private boolean isBound = false;
private boolean isBindingToService = false;
private BroadcastReceiver receiver;
TextView statusBar, statsBar;
/** Defines callbacks for service binding, passed to bindService() */
private ServiceConnection serviceConn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Logger.d("onServiceConnected called");
// We've bound to LocalService, cast the IBinder and get LocalService
// instance
SchedulerBinder binder = (SchedulerBinder) service;
scheduler = binder.getService();
isBound = true;
isBindingToService = false;
initializeStatusBar();
SpeedometerApp.this.sendBroadcast(new UpdateIntent("",
UpdateIntent.SCHEDULER_CONNECTED_ACTION));
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
Logger.d("onServiceDisconnected called");
isBound = false;
}
};
/** Returns the scheduler singleton instance. Should only be called from the UI thread. */
public MeasurementScheduler getScheduler() {
if (isBound) {
return this.scheduler;
} else {
bindToService();
return null;
}
}
/** Returns the tab host. Allows child tabs to request focus changes, etc... */
public TabHost getSpeedomterTabHost() {
return tabHost;
}
private void setPauseIconBasedOnSchedulerState(MenuItem item) {
if (this.scheduler != null && item != null) {
if (this.scheduler.isPauseRequested()) {
item.setIcon(android.R.drawable.ic_media_play);
item.setTitle(R.string.menuResume);
} else {
item.setIcon(android.R.drawable.ic_media_pause);
item.setTitle(R.string.menumPause);
}
}
}
/** Populate the application menu. Only called once per onCreate() */
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main_menu, menu);
return true;
}
/** Adjust menu items depending on system state. Called every time the
* menu pops up */
@Override
public boolean onPrepareOptionsMenu (Menu menu) {
setPauseIconBasedOnSchedulerState(menu.findItem(R.id.menuPauseResume));
return true;
}
/** React to menu item selections */
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.menuPauseResume:
if (this.scheduler != null) {
if (this.scheduler.isPauseRequested()) {
this.scheduler.resume();
} else {
this.scheduler.pause();
}
}
return true;
case R.id.menuQuit: {
Logger.i("User requests exit. Quitting the app");
quitApp();
return true;
}
case R.id.menuSettings: {
Intent settingsActivity = new Intent(getBaseContext(), SpeedometerPreferenceActivity.class);
startActivity(settingsActivity);
return true;
}
case R.id.aboutPage: {
Intent intent = new Intent(getBaseContext(), com.mobiperf.About.class);
startActivity(intent);
return true;
}
case R.id.menuLog: {
Intent intent = new Intent(getBaseContext(), SystemConsoleActivity.class);
startActivity(intent);
return true;
}
default:
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Logger.d("onCreate called");
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
restoreDefaultAccount();
if (selectedAccount == null) {
showDialog(DIALOG_ACCOUNT_SELECTOR);
} else {
// double check the user consent selection
consentDialogWrapper();
}
/* Set the DNS cache TTL to 0 such that measurements can be more accurate.
* However, it is known that the current Android OS does not take actions
* on these properties but may enforce them in future versions.
*/
System.setProperty("networkaddress.cache.ttl", "0");
System.setProperty("networkaddress.cache.negative.ttl", "0");
Security.setProperty("networkaddress.cache.ttl", "0");
Security.setProperty("networkaddress.cache.negative.ttl", "0");
Resources res = getResources(); // Resource object to get Drawables
tabHost = getTabHost(); // The activity TabHost
TabHost.TabSpec spec; // Resusable TabSpec for each tab
Intent intent; // Reusable Intent for each tab
// Do the same for the other tabs
intent = new Intent().setClass(this, MeasurementCreationActivity.class);
spec = tabHost.newTabSpec(MeasurementCreationActivity.TAB_TAG).setIndicator(
"Measure", res.getDrawable(R.drawable.ic_tab_user_measurement)).setContent(intent);
tabHost.addTab(spec);
// Creates the user task console tab
intent = new Intent().setClass(this, ResultsConsoleActivity.class);
spec = tabHost.newTabSpec(ResultsConsoleActivity.TAB_TAG).setIndicator(
"Results", res.getDrawable(R.drawable.ic_tab_results_icon)).setContent(intent);
tabHost.addTab(spec);
// Creates the measurement schedule console tab
intent = new Intent().setClass(this, MeasurementScheduleConsoleActivity.class);
spec = tabHost.newTabSpec(MeasurementScheduleConsoleActivity.TAB_TAG).setIndicator(
"Task Queue", res.getDrawable(R.drawable.ic_tab_schedules)).setContent(intent);
tabHost.addTab(spec);
tabHost.setCurrentTabByTag(MeasurementCreationActivity.TAB_TAG);
statusBar = (TextView) findViewById(R.id.systemStatusBar);
statsBar = (TextView) findViewById(R.id.systemStatsBar);
// We only need one instance of the scheduler thread
intent = new Intent(this, MeasurementScheduler.class);
this.startService(intent);
this.receiver = new BroadcastReceiver() {
@Override
// All onXyz() callbacks are single threaded
public void onReceive(Context context, Intent intent) {
// Update the status bar on SYSTEM_STATUS_UPDATE_ACTION intents
String statusMsg = intent.getStringExtra(UpdateIntent.STATUS_MSG_PAYLOAD);
if (statusMsg != null) {
updateStatusBar(statusMsg);
} else if (scheduler != null) {
initializeStatusBar();
}
String statsMsg = intent.getStringExtra(UpdateIntent.STATS_MSG_PAYLOAD);
if (statsMsg != null) {
updateStatsBar(statsMsg);
}
}
};
IntentFilter filter = new IntentFilter();
filter.addAction(UpdateIntent.SYSTEM_STATUS_UPDATE_ACTION);
this.registerReceiver(this.receiver, filter);
}
protected Dialog onCreateDialog(int id) {
Logger.d("onCreateDialog called");
switch(id) {
case DIALOG_CONSENT:
return showConsentDialog();
case DIALOG_ACCOUNT_SELECTOR:
return showAccountDialog();
default:
return null;
}
}
private void initializeStatusBar() {
if (this.scheduler.isPauseRequested()) {
updateStatusBar(SpeedometerApp.this.getString(R.string.pauseMessage));
} else if (!scheduler.hasBatteryToScheduleExperiment()) {
updateStatusBar(SpeedometerApp.this.getString(R.string.powerThreasholdReachedMsg));
} else {
MeasurementTask currentTask = scheduler.getCurrentTask();
if (currentTask != null) {
if (currentTask.getDescription().priority == MeasurementTask.USER_PRIORITY) {
updateStatusBar("User task " + currentTask.getDescriptor() + " is running");
} else {
updateStatusBar("System task " + currentTask.getDescriptor() + " is running");
}
} else {
updateStatusBar(SpeedometerApp.this.getString(R.string.resumeMessage));
}
}
}
private void updateStatusBar(String statusMsg) {
if (statusMsg != null) {
statusBar.setText(statusMsg);
}
}
private void updateStatsBar(String statsMsg) {
if (statsMsg != null) {
statsBar.setText(statsMsg);
}
}
private void bindToService() {
if (!isBindingToService && !isBound) {
// Bind to the scheduler service if it is not bounded
Intent intent = new Intent(this, MeasurementScheduler.class);
bindService(intent, serviceConn, Context.BIND_AUTO_CREATE);
isBindingToService = true;
}
}
private Dialog showAccountDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Select Authentication Account");
final CharSequence[] items = AccountSelector.getAccountList(this.getApplicationContext());
builder.setCancelable(false)
.setSingleChoiceItems(items, -1, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int item) {
Toast.makeText(getApplicationContext(),
items[item] + " " + getString(R.string.selectedString), Toast.LENGTH_SHORT).show();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
SharedPreferences.Editor editor = prefs.edit();
editor.putString(Config.PREF_KEY_SELECTED_ACCOUNT, (String) items[item]);
editor.commit();
dialog.dismiss();
// need consent dialog when user first perform the account selection
consentDialogWrapper();
}
});
return builder.create();
}
private Dialog showConsentDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
final TextView message = new TextView(this);
final SpannableString s = new SpannableString(getString(R.string.terms));
Linkify.addLinks(s, Linkify.WEB_URLS);
message.setText(s);
message.setTextColor(Color.WHITE);
message.setLinkTextColor(Color.CYAN);
message.setTextSize(17);
message.setMovementMethod(LinkMovementMethod.getInstance());
builder.setView(message)
.setCancelable(false)
.setPositiveButton("Okay, got it", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
recordUserConsent();
// Enable auto start on boot.
setStartOnBoot(true);
// Force a checkin now since the one initiated by the scheduler was likely skipped.
doCheckin();
}
})
.setNegativeButton("No thanks", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
quitApp();
}
});
return builder.create();
}
@Override
protected void onStart() {
Logger.d("onStart called");
// Bind to the scheduler service for only once during the lifetime of the activity
bindToService();
super.onStart();
}
@Override
protected void onStop() {
Logger.d("onStop called");
super.onStop();
if (isBound) {
unbindService(serviceConn);
isBound = false;
}
}
@Override
protected void onDestroy() {
Logger.d("onDestroy called");
super.onDestroy();
this.unregisterReceiver(this.receiver);
}
private void quitApp() {
Logger.d("quitApp called");
if (isBound) {
unbindService(serviceConn);
isBound = false;
}
if (this.scheduler != null) {
Logger.d("requesting Scheduler stop");
scheduler.requestStop();
}
// Force consent on next restart.
userConsented = false;
saveConsentState();
// Disable auto start on boot.
setStartOnBoot(false);
this.finish();
System.exit(0);
}
private void doCheckin() {
if (scheduler != null) {
scheduler.handleCheckin(true);
}
}
/** Set preference to indicate whether start on boot is enabled. */
private void setStartOnBoot(boolean startOnBoot) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext());
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(getString(R.string.startOnBootPrefKey), startOnBoot);
editor.commit();
}
private void recordUserConsent() {
userConsented = true;
saveConsentState();
}
/** Save consent state persistent storage. */
private void saveConsentState() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext());
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(Config.PREF_KEY_CONSENTED, userConsented);
editor.commit();
}
/**
* Restore the last used account
*/
private void restoreDefaultAccount() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
selectedAccount = prefs.getString(Config.PREF_KEY_SELECTED_ACCOUNT, null);
}
/**
* Restore measurement statistics from persistent storage.
*/
private void restoreConsentState() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext());
userConsented = prefs.getBoolean(Config.PREF_KEY_CONSENTED, false);
}
/**
* A wrapper function to check user consent selection,
* and generate one if user haven't agreed on.
*/
private void consentDialogWrapper() {
restoreConsentState();
if (!userConsented) {
// Show the consent dialog. After user select the content
showDialog(DIALOG_CONSENT);
}
}
}