package edu.berkeley.cs.amplab.carat.android; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Properties; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.preference.PreferenceManager; import android.support.v4.app.ActionBarDrawerToggle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager.BackStackEntry; import android.support.v4.app.FragmentTransaction; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarActivity; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.widget.AdapterView; import android.widget.ListView; import android.widget.Toast; import com.flurry.android.FlurryAgent; import edu.berkeley.cs.amplab.carat.android.fragments.AboutFragment; import edu.berkeley.cs.amplab.carat.android.fragments.BugsOrHogsFragment; import edu.berkeley.cs.amplab.carat.android.fragments.CaratSettingsFragment; import edu.berkeley.cs.amplab.carat.android.fragments.EnableInternetDialogFragment; import edu.berkeley.cs.amplab.carat.android.fragments.MyDeviceFragment; import edu.berkeley.cs.amplab.carat.android.fragments.SuggestionsFragment; import edu.berkeley.cs.amplab.carat.android.fragments.SummaryFragment; import edu.berkeley.cs.amplab.carat.android.sampling.SamplingLibrary; import edu.berkeley.cs.amplab.carat.android.subscreens.WebViewFragment; import edu.berkeley.cs.amplab.carat.android.utils.JsonParser; import edu.berkeley.cs.amplab.carat.android.utils.Tracker; /** * Carat Android App Main Activity. Is loaded right after CaratApplication. * Holds the Tabs that comprise the UI. Place code related to tab handling and * global Activity code here. * * @author Eemil Lagerspetz * */ public class MainActivity extends ActionBarActivity { private DrawerLayout mDrawerLayout; private ListView mDrawerList; private ActionBarDrawerToggle mDrawerToggle; private CharSequence mTitle; private String[] mDrawerItems; // Log tag // private static final String TAG = "MainActivity"; public static final String ACTION_BUGS = "bugs", ACTION_HOGS = "hogs"; // Key File private static final String FLURRY_KEYFILE = "flurry.properties"; private String fullVersion = null; private Tracker tracker = null; /** * Dynamic way of dealing with a list of fragments that you need to keep references for. */ private Fragment[] frags = new Fragment[CaratApplication.getTitles().length]; private Bundle mArgs; // public boolean updateSummaryFragment; // counts (general Carat statistics shown in the summary fragment) public int mWellbehaved = Constants.VALUE_NOT_AVAILABLE, mHogs = Constants.VALUE_NOT_AVAILABLE, mBugs = Constants.VALUE_NOT_AVAILABLE ; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); CaratApplication.setMain(this); tracker = Tracker.getInstance(); // track user clicks (taps) tracker.trackUser("caratstarted"); if (!CaratApplication.isInternetAvailable()) { EnableInternetDialogFragment dialog = new EnableInternetDialogFragment(); dialog.show(getSupportFragmentManager(), "dialog"); } /* * Activity.getWindow.requestFeature() should get invoked only before * setContentView(), otherwise it will cause an app crash The progress * bar doesn't get displayed when there is no update in progress */ getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS); getWindow().requestFeature(Window.FEATURE_PROGRESS); // Log.d(TAG, "about to set the layout"); setContentView(R.layout.activity_main); ActionBar actionBar = getSupportActionBar(); setTitleNormal(); // read and load the preferences specified in our xml preference file PreferenceManager.setDefaultValues(this, R.xml.preferences, false); // Log.d(TAG, "about to initialize fragments"); preInittializeFragments(); // Log.d(TAG, "done with fragment initialization"); /* * Before using the field "fullVersion", first invoke setTitleNormal() * or setFullVersion() to set this field */ mDrawerItems = getResources().getStringArray(R.array.drawer_items); List<Item> items = new ArrayList<Item>(); // items.add(new NavDrawerListHeader("Main")); items.add(new ListItem(mDrawerItems[0])); items.add(new ListItem(mDrawerItems[1])); items.add(new ListItem(mDrawerItems[2])); items.add(new ListItem(mDrawerItems[3])); items.add(new ListItem(mDrawerItems[4])); items.add(new NavDrawerListHeader("")); items.add(new ListItem(mDrawerItems[5])); items.add(new ListItem(mDrawerItems[6])); TextArrayAdapter adapter = new TextArrayAdapter(this, items); mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); mDrawerList = (ListView) findViewById(R.id.left_drawer); // set a custom shadow that overlays the main content when the drawer opens mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); // set up the drawer's list view with items and click listener mDrawerList.setAdapter(adapter); // mDrawerList.setAdapter(new ArrayAdapter<String>(this, R.layout.drawer_list_item, mDrawerItems)); mDrawerList.setOnItemClickListener(new DrawerItemClickListener()); // ActionBarDrawerToggle ties together the the proper interactions // between the sliding drawer and the action bar app icon mDrawerToggle = new ActionBarDrawerToggle( this, /* host Activity */ mDrawerLayout, /* DrawerLayout object */ R.drawable.ic_drawer, /* nav drawer image to replace 'Up' caret */ R.string.drawer_open, /* "open drawer" description for accessibility */ R.string.drawer_close /* "close drawer" description for accessibility */ ) { public void onDrawerClosed(View view) { //getSupportActionBar().setTitle(mTitle); } public void onDrawerOpened(View drawerView) { getSupportActionBar().setTitle(mTitle); } }; mDrawerLayout.setDrawerListener(mDrawerToggle); // Enable ActionBar app icon to behave as action to toggle navigation drawer actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeButtonEnabled(true); if (savedInstanceState == null) { selectItem(0); } setTitleNormal(); // Uncomment the following to enable listening on local port 8080: /* * try { * HelloServer h = new HelloServer(); * } catch (IOException e) { * e.printStackTrace(); * } */ } /* The click listener for ListView in the navigation drawer */ private class DrawerItemClickListener implements ListView.OnItemClickListener { @Override public void onItemClick(AdapterView<?> parent, View view, final int position, long id) { // To remove the lag in closing the drawer, don't do a fragment transaction // while the drawer is getting closed. // First close the drawer (takes about 300ms (transition/animation time)), // wait for it to get closed completely, then start replacing the fragment . // How to modify the navigation drawer closing transition time: // http://stackoverflow.com/questions/19460683/speed-up-navigation-drawer-animation-speed-on-closing // consider menu item headers (additional menu items) when selecting an item // TODO: use a dynamic approach final int newPosition = (position <= 4) ? position : position - 1; mDrawerLayout.closeDrawer(mDrawerList); new Handler().postDelayed(new Runnable() { @Override public void run() { selectItem(newPosition); } }, 300); // wait 300ms before calling selectItem() } } private void selectItem(int position) { // update the main content by replacing fragments replaceFragment(frags[position], mDrawerItems[position], true); mDrawerList.setItemChecked(position, true); } /** * * @param index 0-based index of the navigation drawer entries (e.g. 3 for bugs fragment) * @return the tag of the fragment corresponding to the index */ public String getFragmentTag(int index) { return mDrawerItems[index]; } /** * Avoid displaying a white screen when the back button is pressed in the summary fragment. * When we are in the summary fragment, since there is only one fragment in the backstack, * the fragment manager will fail to pop another fragment from the backstack, * so only the framelayout (the parent/host widget for fragments (in our activity's layout)) is shown. * We need to check the number of fragments present in the backstack, and act accordingly */ @Override public void onBackPressed() { FragmentManager manager = getSupportFragmentManager(); // If we will pop a top level screen, show drawer indicator again int stackTop = manager.getBackStackEntryCount()-1; BackStackEntry entry = manager.getBackStackEntryAt(stackTop); String name = entry.getName(); String[] titles = CaratApplication.getTitles(); boolean found = false; for (String t: titles){ if (!found) found = t.equals(name); } if (found){ // Restore menu mDrawerToggle.setDrawerIndicatorEnabled(true); } if (stackTop > 0 ) { // If there are back-stack entries, replace the fragment (go to the fragment) manager.popBackStack(); }else finish(); } @Override public void setTitle(CharSequence title) { getSupportActionBar().setTitle(title); } /** * When using the ActionBarDrawerToggle, you must call it during * onPostCreate() and onConfigurationChanged()... */ @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Sync the toggle state after onRestoreInstanceState has occurred. mDrawerToggle.syncState(); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); // Pass any configuration change to the drawer toggle mDrawerToggle.onConfigurationChanged(newConfig); } @Override public boolean onOptionsItemSelected(MenuItem item) { // Pass the event to ActionBarDrawerToggle, if it returns // true, then it has handled the app icon touch event // In case we are at a sublevel, enable going back by clicking title. if (item.getItemId() == android.R.id.home && !mDrawerToggle.isDrawerIndicatorEnabled()){ onBackPressed(); return true; } if (mDrawerToggle.onOptionsItemSelected(item)) { return true; } // Handle your other action bar items... return super.onOptionsItemSelected(item); } public void setTitleNormal() { setFullVersion(); if (CaratApplication.storage != null) { long s = CaratApplication.storage.getSamplesReported(); Log.d("setTitleNormal", "number of samples reported=" + String.valueOf(s)); if (s > 0) { mTitle = fullVersion + " - "+ s + " " + getString(R.string.samplesreported); } else { mTitle = fullVersion; } } setTitle(mTitle); } private void setFullVersion() { fullVersion = getString(R.string.app_name) + " " + getString(R.string.version_name); } public String getFullVersion() { return fullVersion; } public void setTitleUpdating(String what) { setTitle(getString(R.string.updating) + " " + what); // this is quick hack to update SummaryFragment. if (what.equals(getString(R.string.finishing))) refreshSummaryFragment(); } public void setTitleUpdatingFailed(String what) { setTitle(getString(R.string.didntget) + " " + what); } /** * Used in the system settings fragment and the summary fragment * @param intentString * @param thing */ public void safeStart(String intentString, String thing) { Intent intent = null; try { intent = new Intent(intentString); startActivity(intent); } catch (Throwable th) { // Log.e(TAG, "Could not start activity: " + intent, th); if (thing != null) { Toast t = Toast.makeText(this, getString(R.string.opening) + thing + getString(R.string.notsupported), Toast.LENGTH_SHORT); t.show(); } } } @Override protected void onStart() { super.onStart(); String secretKey = null; Properties properties = new Properties(); try { InputStream raw = MainActivity.this.getAssets().open(FLURRY_KEYFILE); if (raw != null) { properties.load(raw); if (properties.containsKey("secretkey")) secretKey = properties.getProperty("secretkey", "secretkey"); // Log.d(TAG, "Set Flurry secret key."); } else { // Log.e(TAG, "Could not open Flurry key file!"); } } catch (IOException e) { // Log.e(TAG, "Could not open Flurry key file: " + e.toString()); } if (secretKey != null) { FlurryAgent.onStartSession(getApplicationContext(), secretKey); } /* To perform an action when our defaultSharedPreferences changes, we can do it in this listener * when you uncomment this, remember to uncomment the unregistering code in the onStop() method of this activity (right below) */ // PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener); } @Override protected void onStop() { super.onStop(); FlurryAgent.onEndSession(getApplicationContext()); // PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(mOnSharedPreferenceChangeListener); } @Override protected void onResume() { // Log.i(TAG, "Resumed. Refreshing UI"); tracker.trackUser("caratresumed"); // if statistics data for the summary fragment is not already fetched, // and the device has an Internet connection, fetch statistics and then refresh the summary fragment if ( (! isStatsDataAvailable()) && CaratApplication.isInternetAvailable()) { getStatsFromServer(); } /** * This may take minutes, so refresh summary frag here again. */ new Thread() { public void run() { ((CaratApplication) getApplication()).refreshUi(); // This should only run if we are on that tab, so onResume of SummaryFragment should be enough. //refreshSummaryFragment(); }}.start(); super.onResume(); } public Fragment getVisibleFragment(){ FragmentManager fragmentManager = MainActivity.this.getSupportFragmentManager(); List<Fragment> fragments = fragmentManager.getFragments(); for(Fragment fragment : fragments){ if(fragment != null && fragment.isVisible()) return fragment; } return null; } @Override protected void onPause() { // Log.i(TAG, "Paused"); tracker.trackUser("caratpaused"); SamplingLibrary.resetRunningProcessInfo(); super.onPause(); } @Override public void finish() { // Log.d(TAG, "Finishing up"); tracker.trackUser("caratstopped"); super.finish(); } /** * must be called in onCreate() method of the activity, before calling selectItem() method * [before attaching the navigation drawer listener] * pre-initialize all fragments before committing a replace fragment transaction * may help for better smoothness when user selects a navigation drawer item */ private void preInittializeFragments() { getStatsFromServer(); // after fetching the data needed by the summary fragment, initialize it int idx = 0; frags[idx] = new SummaryFragment(); idx++; frags[idx] = new SuggestionsFragment(); idx++; frags[idx] = new MyDeviceFragment(); idx++; frags[idx] = new BugsOrHogsFragment(); mArgs = new Bundle(); mArgs.putBoolean("isBugs", true); frags[idx].setArguments(mArgs); idx++; frags[idx] = new BugsOrHogsFragment(); mArgs = new Bundle(); mArgs.putBoolean("isBugs", false); frags[idx].setArguments(mArgs); idx++; frags[idx] = new CaratSettingsFragment(); idx++; frags[idx] = new AboutFragment(); } /** * Before initializing the summary fragment, we need to fetch the the data it needs from our server, * in an asyncTask in a new thread */ @SuppressLint("NewApi") private void getStatsFromServer() { PrefetchData prefetchData = new PrefetchData(); // run this asyncTask in a new thread [from the thread pool] (run in parallel to other asyncTasks) // (do not wait for them to finish, it takes a long time) if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB) prefetchData.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); else prefetchData.execute(); } /* * shows the fragment using a fragment transaction (replaces the FrameLayout * (a placeholder in the main activity's layout file) with the passed-in fragment) * * @param fragment the fragment that should be shown * @param tag a name for the fragment to be shown in the * fragment (task) stack */ public void replaceFragment(Fragment fragment, String tag, boolean showDrawerIndicator) { // use a fragment tag, so that later on we can find the currently displayed fragment final String FRAGMENT_TAG = tag; // replace the fragment, using a fragment transaction FragmentManager fragmentManager = getSupportFragmentManager(); FragmentTransaction transaction = fragmentManager.beginTransaction(); transaction.replace(R.id.content_frame, fragment, FRAGMENT_TAG) .addToBackStack(FRAGMENT_TAG) .commit(); mDrawerToggle.setDrawerIndicatorEnabled(showDrawerIndicator); } /** * used by other classes * @param fileName */ public void showHTMLFile(String fileName, String title, boolean showDrawerIndicator) { WebViewFragment fragment = WebViewFragment.getInstance(fileName); replaceFragment(fragment, title, showDrawerIndicator); } public boolean isStatsDataAvailable() { if (isStatsDataLoaded()) { // Log.i(TAG, "isStatsDataAvailable(), mWellbehaved=" + mWellbehaved + ", mHogs=" + mHogs + ", mBugs=" + mBugs); return true; } else { return isStatsDataStoredInPref(); } } private boolean isStatsDataLoaded() { // don't check for zero, check for something unlikely, e.g. -1 (use a constant for that value, use it consistently) return mWellbehaved != Constants.VALUE_NOT_AVAILABLE && mHogs != Constants.VALUE_NOT_AVAILABLE && mBugs != Constants.VALUE_NOT_AVAILABLE; } private boolean isStatsDataStoredInPref() { // TODO: consider a data freshness timeout (e.g. two weeks) int wellbehaved = CaratApplication.mPrefs.getInt(Constants.STATS_WELLBEHAVED_COUNT_PREFERENCE_KEY, Constants.VALUE_NOT_AVAILABLE); int hogs = CaratApplication.mPrefs.getInt(Constants.STATS_HOGS_COUNT_PREFERENCE_KEY, Constants.VALUE_NOT_AVAILABLE); int bugs = CaratApplication.mPrefs.getInt(Constants.STATS_BUGS_COUNT_PREFERENCE_KEY, Constants.VALUE_NOT_AVAILABLE); if (wellbehaved != Constants.VALUE_NOT_AVAILABLE && hogs != Constants.VALUE_NOT_AVAILABLE && bugs != Constants.VALUE_NOT_AVAILABLE) { // Log.i(TAG, "isStatsDataAvailable(), wellbehaved (fetched from the pref)=" + wellbehaved); mWellbehaved = wellbehaved; mHogs = hogs; mBugs = bugs; return true; } else { return false; } } public void GoToWifiScreen() { safeStart(android.provider.Settings.ACTION_WIFI_SETTINGS, getString(R.string.wifisettings)); } public void refreshSummaryFragment() { if (frags.length > 0) ((SummaryFragment) frags[0]).scheduleRefresh(); } public Fragment getMydeviceFragment() { return frags[2]; } public Fragment getHogsFragment() { return frags[4]; } public void setHogsFragment(Fragment hogsFragment) { frags[4] = hogsFragment; } public Fragment getBugsFragment() { return frags[3]; } public void setBugsFragment(Fragment bugsFragment) { frags[3] = bugsFragment; } /** * A listener that is triggered when a value changes in our defualtSharedPreferences. * Can be used to do an immediate action whenever one of our items in that hashtable (defualtSharedPreferences) changes. * Should be registered (in our main activity's onStart()) and unregistered (in onStop()) */ // private OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { // @Override // public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { // Toast.makeText(CaratApplication.getMainActivity(), String.valueOf(sharedPreferences.getBoolean(key, false)), Toast.LENGTH_SHORT).show(); // } // }; public class PrefetchData extends AsyncTask<Void, Void, Void> { String serverResponseJson = null; private final String TAG = "PrefetchData"; @Override protected Void doInBackground(Void... arg0) { // Log.d(TAG, "started doInBackground() method of the asyncTask"); JsonParser jsonParser = new JsonParser(); try { if (CaratApplication.isInternetAvailable()) { serverResponseJson = jsonParser .getJSONFromUrl("http://carat.cs.helsinki.fi/statistics-data/stats.json"); } } catch (Exception e) { } if (serverResponseJson != null && serverResponseJson != "") { try { JSONArray jsonArray = new JSONObject(serverResponseJson).getJSONArray("android-apps"); // Using Java reflections to set fields by passing their name to a method try { setIntFieldsFromJson(jsonArray, 0, "mWellbehaved"); setIntFieldsFromJson(jsonArray, 1, "mHogs"); setIntFieldsFromJson(jsonArray, 2, "mBugs"); if (CaratApplication.mPrefs != null) { saveStatsToPref(); } else { // Log.e(TAG, "The shared preference is null (not loaded yet. " // + "Check CaratApplication's new thread for loading the sharedPref)"); } // Log.i(TAG, "received JSON: " + "mBugs: " + mWellbehaved // + ", mHogs: " + mHogs + ", mBugs: " + mBugs); } catch (IllegalArgumentException e) { Log.e(TAG, "IllegalArgumentException in setFieldsFromJson()"); } catch (IllegalAccessException e) { Log.e(TAG, "IllegalAccessException in setFieldsFromJson()"); } } catch (JSONException e) { // Log.e(TAG, e.getStackTrace().toString()); } } else { // Log.d(TAG, "server response JSON is null."); } return null; } @SuppressLint("NewApi") private void saveStatsToPref() { SharedPreferences.Editor editor = CaratApplication.mPrefs.edit(); // the returned values (from setIntFieldsFromJson() // might be -1 (Constants.VALUE_NOT_AVAILABLE). So // when we are reading the following pref values, we // should check that condition ) editor.putInt(Constants.STATS_WELLBEHAVED_COUNT_PREFERENCE_KEY, mWellbehaved); editor.putInt(Constants.STATS_HOGS_COUNT_PREFERENCE_KEY, mHogs); editor.putInt(Constants.STATS_BUGS_COUNT_PREFERENCE_KEY, mBugs); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { editor.apply(); // async (runs in parallel // in a new shared thread (off the UI thread) } else { editor.commit(); } } @Override protected void onPostExecute(Void result) { // Log.d(TAG, "started the onPostExecute() of the asyncTask"); super.onPostExecute(result); // SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss"); // sdf.setTimeZone(TimeZone.getTimeZone("GMT")); // Log.d(TAG, "asyncTask.onPstExecute(). mWellbehaved=" + mWellbehaved); refreshSummaryFragment(); } /** * Using Java reflections to set fields by passing their name to a method * @param jsonArray the json array from which we want to extract different json objects * @param objIdx the index of the object in the json array * @param fieldName the name of the field in the current NESTED class (PrefetchData) */ private void setIntFieldsFromJson(JSONArray jsonArray, int objIdx, String fieldName) throws JSONException, IllegalArgumentException, IllegalAccessException { // Class<? extends PrefetchData> currentClass = this.getClass(); Field field = null; int res = Constants.VALUE_NOT_AVAILABLE; try { // important: getField() can only get PUBLIC fields. // For private fields, use another method: getDeclaredField(fieldName) field = /*currentClass.*/ CaratApplication.getMainActivity().getClass().getField(fieldName); } catch(NoSuchFieldException e) { // Log.e(TAG, "NoSuchFieldException when trying to get a reference to the field: " + fieldName); } if (field != null) { JSONObject jsonObject = null; if (jsonArray != null ) { jsonObject = jsonArray.getJSONObject(objIdx); if (jsonObject != null && jsonObject.getString("value") != null && jsonObject.getString("value") != "") { res = Integer.parseInt(jsonObject.getString("value")); field.set(CaratApplication.getMainActivity()/*this*/, res); } else { // Log.e(TAG, "json object (server response) is null: jsonArray(" + objIdx + ")=null (or ='')"); } } } // if an exception occurs, the value of the field would be -1 (Constants.VALUE_NOT_AVAILABLE) } } /** * Handle physical menu button (e.g. Samsung devices). */ public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_MENU) { boolean drawerOpen = mDrawerLayout.isDrawerOpen(mDrawerList); if (drawerOpen) { mDrawerLayout.closeDrawers(); } else { // FIXME: Gravity.Start is not available in API Level 8, so hack below. int grav = Gravity.TOP|Gravity.LEFT; if (isRTL()) grav = Gravity.TOP|Gravity.RIGHT; mDrawerLayout.openDrawer(grav); } return true; } else { return super.onKeyUp(keyCode, event); } } private static boolean isRTL() { return isRTL(Locale.getDefault()); } private static boolean isRTL(Locale locale) { final int directionality = Character.getDirectionality(locale.getDisplayName().charAt(0)); return directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT || directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC; } }