package com.nutomic.syncthingandroid.activities; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.PowerManager; import android.preference.PreferenceManager; import android.provider.Settings; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.GravityCompat; import android.support.v4.view.ViewPager; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarDrawerToggle; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.nutomic.syncthingandroid.R; import com.nutomic.syncthingandroid.fragments.DeviceListFragment; import com.nutomic.syncthingandroid.fragments.DrawerFragment; import com.nutomic.syncthingandroid.fragments.FolderListFragment; import com.nutomic.syncthingandroid.model.Options; import com.nutomic.syncthingandroid.service.SyncthingService; import java.util.Date; import java.util.concurrent.TimeUnit; import static java.lang.Math.min; /** * Shows {@link FolderListFragment} and * {@link DeviceListFragment} in different tabs, and * {@link DrawerFragment} in the navigation drawer. */ public class MainActivity extends SyncthingActivity implements SyncthingService.OnApiChangeListener { private static final String TAG = "MainActivity"; private static final String IS_SHOWING_RESTART_DIALOG = "RESTART_DIALOG_STATE"; private static final String BATTERY_DIALOG_DISMISSED = "BATTERY_DIALOG_STATE"; /** * Time after first start when usage reporting dialog should be shown. * * @see #showUsageReportingDialog() */ private static final long USAGE_REPORTING_DIALOG_DELAY = TimeUnit.DAYS.toMillis(3); private AlertDialog mDisabledDialog; private AlertDialog mBatteryOptimizationsDialog; private Dialog mRestartDialog; private boolean mBatteryOptimizationDialogDismissed; private ViewPager mViewPager; private FolderListFragment mFolderListFragment; private DeviceListFragment mDeviceListFragment; private DrawerFragment mDrawerFragment; private ActionBarDrawerToggle mDrawerToggle; private DrawerLayout mDrawerLayout; /** * Handles various dialogs based on current state. */ @Override public void onApiChange(SyncthingService.State currentState) { switch (currentState) { case STARTING: dismissDisabledDialog(); break; case ACTIVE: dismissDisabledDialog(); showBatteryOptimizationDialogIfNecessary(); mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED); mDrawerFragment.requestGuiUpdate(); if (new Date().getTime() > getFirstStartTime() + USAGE_REPORTING_DIALOG_DELAY && getApi().getOptions().getUsageReportValue() == Options.USAGE_REPORTING_UNDECIDED) { showUsageReportingDialog(); } break; case ERROR: finish(); break; case DISABLED: if (!isFinishing()) { showDisabledDialog(); } break; } } @TargetApi(23) private void showBatteryOptimizationDialogIfNecessary() { SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); boolean dontShowAgain = sp.getBoolean("battery_optimization_dont_show_again", false); if (dontShowAgain || mBatteryOptimizationsDialog != null || Build.VERSION.SDK_INT < Build.VERSION_CODES.M || pm.isIgnoringBatteryOptimizations(getPackageName()) || mBatteryOptimizationDialogDismissed) { return; } mBatteryOptimizationsDialog = new AlertDialog.Builder(this) .setTitle(R.string.dialog_disable_battery_optimization_title) .setMessage(R.string.dialog_disable_battery_optimization_message) .setPositiveButton(R.string.dialog_disable_battery_optimization_turn_off, (d, i) -> { Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); intent.setData(Uri.parse("package:" + getPackageName())); startActivity(intent); }) .setNeutralButton(R.string.dialog_disable_battery_optimization_later, (d, i) -> mBatteryOptimizationDialogDismissed = true) .setNegativeButton(R.string.dialog_disable_battery_optimization_dont_show_again, (d, i) -> { sp.edit().putBoolean("battery_optimization_dont_show_again", true).apply(); }) .setOnCancelListener(d -> mBatteryOptimizationDialogDismissed = true) .show(); } private void dismissDisabledDialog() { if (mDisabledDialog != null) { mDisabledDialog.dismiss(); mDisabledDialog = null; } } /** * Returns the unix timestamp at which the app was first installed. */ private long getFirstStartTime() { PackageManager pm = getPackageManager(); long firstInstallTime = 0; try { firstInstallTime = pm.getPackageInfo(getPackageName(), 0).firstInstallTime; } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "This should never happen", e); } return firstInstallTime; } private final FragmentPagerAdapter mSectionsPagerAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) { @Override public Fragment getItem(int position) { switch (position) { case 0: return mFolderListFragment; case 1: return mDeviceListFragment; default: return null; } } @Override public int getCount() { return 2; } @Override public CharSequence getPageTitle(int position) { switch (position) { case 0: return getResources().getString(R.string.folders_fragment_title); case 1: return getResources().getString(R.string.devices_fragment_title); default: return String.valueOf(position); } } }; /** * Initializes tab navigation. */ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); mViewPager = (ViewPager) findViewById(R.id.pager); mViewPager.setAdapter(mSectionsPagerAdapter); TabLayout tabLayout = (TabLayout) findViewById(R.id.tabContainer); tabLayout.setupWithViewPager(mViewPager); if (savedInstanceState != null) { FragmentManager fm = getSupportFragmentManager(); mFolderListFragment = (FolderListFragment) fm.getFragment( savedInstanceState, FolderListFragment.class.getName()); mDeviceListFragment = (DeviceListFragment) fm.getFragment( savedInstanceState, DeviceListFragment.class.getName()); mDrawerFragment = (DrawerFragment) fm.getFragment( savedInstanceState, DrawerFragment.class.getName()); mViewPager.setCurrentItem(savedInstanceState.getInt("currentTab")); if (savedInstanceState.getBoolean(IS_SHOWING_RESTART_DIALOG)){ showRestartDialog(); } mBatteryOptimizationDialogDismissed = savedInstanceState.getBoolean(BATTERY_DIALOG_DISMISSED); } else { mFolderListFragment = new FolderListFragment(); mDeviceListFragment = new DeviceListFragment(); mDrawerFragment = new DrawerFragment(); } getSupportFragmentManager() .beginTransaction() .replace(R.id.drawer, mDrawerFragment) .commit(); mDrawerToggle = new Toggle(this, mDrawerLayout); mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); mDrawerLayout.addDrawerListener(mDrawerToggle); setOptimalDrawerWidth(findViewById(R.id.drawer)); onNewIntent(getIntent()); } @Override public void onDestroy() { super.onDestroy(); dismissDisabledDialog(); if (getService() != null) { getService().unregisterOnApiChangeListener(this); getService().unregisterOnApiChangeListener(mFolderListFragment); getService().unregisterOnApiChangeListener(mDeviceListFragment); } } @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { super.onServiceConnected(componentName, iBinder); getService().registerOnApiChangeListener(this); getService().registerOnApiChangeListener(mFolderListFragment); getService().registerOnApiChangeListener(mDeviceListFragment); } /** * Saves current tab index and fragment states. */ @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // Avoid crash if called during startup. if (mFolderListFragment != null && mDeviceListFragment != null && mDrawerFragment != null) { FragmentManager fm = getSupportFragmentManager(); fm.putFragment(outState, FolderListFragment.class.getName(), mFolderListFragment); fm.putFragment(outState, DeviceListFragment.class.getName(), mDeviceListFragment); fm.putFragment(outState, DrawerFragment.class.getName(), mDrawerFragment); outState.putInt("currentTab", mViewPager.getCurrentItem()); outState.putBoolean(BATTERY_DIALOG_DISMISSED, mBatteryOptimizationsDialog == null || !mBatteryOptimizationsDialog.isShowing()); outState.putBoolean(IS_SHOWING_RESTART_DIALOG, mRestartDialog != null && mRestartDialog.isShowing()); if (mRestartDialog != null){ mRestartDialog.cancel(); } } } @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); mDrawerToggle.syncState(); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setHomeButtonEnabled(true); } } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mDrawerToggle.onConfigurationChanged(newConfig); } private void showDisabledDialog() { mDisabledDialog = new AlertDialog.Builder(this) .setTitle(R.string.syncthing_disabled_title) .setMessage(R.string.syncthing_disabled_message) .setPositiveButton(R.string.syncthing_disabled_change_settings, (dialogInterface, i) -> { finish(); startActivity(new Intent(this, SettingsActivity.class)); } ) .setNegativeButton(R.string.exit, (dialogInterface, i) -> finish() ) .setOnCancelListener(dialogInterface -> finish()) .show(); mDisabledDialog.setCanceledOnTouchOutside(false); } public void showRestartDialog(){ mRestartDialog = createRestartDialog(); mRestartDialog.show(); } private Dialog createRestartDialog(){ return new AlertDialog.Builder(this) .setMessage(R.string.dialog_confirm_restart) .setPositiveButton(android.R.string.yes, (dialogInterface, i1) -> this.startService(new Intent(this, SyncthingService.class) .setAction(SyncthingService.ACTION_RESTART))) .setNegativeButton(android.R.string.no, null) .create(); } @Override public boolean onOptionsItemSelected(MenuItem item) { return mDrawerToggle.onOptionsItemSelected(item) || super.onOptionsItemSelected(item); } /** * Handles drawer opened and closed events, toggling option menu state. */ private class Toggle extends ActionBarDrawerToggle { public Toggle(Activity activity, DrawerLayout drawerLayout) { super(activity, drawerLayout, R.string.app_name, R.string.app_name); } @Override public void onDrawerOpened(View drawerView) { super.onDrawerOpened(drawerView); mDrawerFragment.onDrawerOpened(); } @Override public void onDrawerClosed(View view) { super.onDrawerClosed(view); mDrawerFragment.onDrawerClosed(); } @Override public void onDrawerSlide(View drawerView, float slideOffset) { super.onDrawerSlide(drawerView, 0); } } /** * Closes the drawer. Use when navigating away from activity. */ public void closeDrawer() { mDrawerLayout.closeDrawer(GravityCompat.START); } /** * Toggles the drawer on menu button press. */ @Override public boolean onKeyDown(int keyCode, KeyEvent e) { if (keyCode == KeyEvent.KEYCODE_MENU) { if (!mDrawerLayout.isDrawerOpen(GravityCompat.START)) mDrawerLayout.openDrawer(GravityCompat.START); else closeDrawer(); return true; } return super.onKeyDown(keyCode, e); } /** * Close drawer on back button press. */ @Override public void onBackPressed() { if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) closeDrawer(); else super.onBackPressed(); } /** * Calculating width based on * http://www.google.com/design/spec/patterns/navigation-drawer.html#navigation-drawer-specs. */ private void setOptimalDrawerWidth(View drawerContainer) { int actionBarSize = 0; TypedValue tv = new TypedValue(); if (getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) { actionBarSize = TypedValue.complexToDimensionPixelSize(tv.data,getResources().getDisplayMetrics()); } ViewGroup.LayoutParams params = drawerContainer.getLayoutParams(); DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); int minScreenWidth = min(displayMetrics.widthPixels, displayMetrics.heightPixels); params.width = min(minScreenWidth - actionBarSize, 5 * actionBarSize); drawerContainer.requestLayout(); } /** * Displays dialog asking user to accept/deny usage reporting. */ private void showUsageReportingDialog() { final DialogInterface.OnClickListener listener = (dialog, which) -> { Options options = getApi().getOptions(); switch (which) { case DialogInterface.BUTTON_POSITIVE: options.urAccepted = Options.USAGE_REPORTING_ACCEPTED; break; case DialogInterface.BUTTON_NEGATIVE: options.urAccepted = Options.USAGE_REPORTING_DENIED; break; case DialogInterface.BUTTON_NEUTRAL: Uri uri = Uri.parse("https://data.syncthing.net"); startActivity(new Intent(Intent.ACTION_VIEW, uri)); break; } getApi().editSettings(getApi().getGui(), options, this); }; getApi().getUsageReport(report -> { @SuppressLint("InflateParams") View v = LayoutInflater.from(MainActivity.this) .inflate(R.layout.dialog_usage_reporting, null); TextView tv = (TextView) v.findViewById(R.id.example); tv.setText(report); new AlertDialog.Builder(MainActivity.this) .setTitle(R.string.usage_reporting_dialog_title) .setView(v) .setPositiveButton(R.string.yes, listener) .setNegativeButton(R.string.no, listener) .setNeutralButton(R.string.open_website, listener) .show(); }); } }