package org.ostrya.presencepublisher; import android.Manifest; import android.annotation.SuppressLint; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.location.LocationManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.PowerManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceManager; import androidx.viewpager.widget.ViewPager; import com.hypertrack.hyperlog.HyperLog; import org.ostrya.presencepublisher.mqtt.Publisher; import org.ostrya.presencepublisher.ui.MainPagerAdapter; import org.ostrya.presencepublisher.ui.dialog.ConfirmationDialogFragment; import java.util.Collections; import static android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS; import static android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS; import static org.ostrya.presencepublisher.Application.*; import static org.ostrya.presencepublisher.ui.preference.condition.AddBeaconChoicePreferenceDummy.BEACON_LIST; import static org.ostrya.presencepublisher.ui.preference.condition.AddNetworkChoicePreferenceDummy.SSID_LIST; import static org.ostrya.presencepublisher.ui.preference.condition.OfflineContentPreference.OFFLINE_CONTENT; import static org.ostrya.presencepublisher.ui.preference.condition.SendOfflineMessagePreference.SEND_OFFLINE_MESSAGE; import static org.ostrya.presencepublisher.ui.preference.condition.SendViaMobileNetworkPreference.SEND_VIA_MOBILE_NETWORK; import static org.ostrya.presencepublisher.ui.preference.condition.WifiNetworkPreference.WIFI_CONTENT_PREFIX; import static org.ostrya.presencepublisher.ui.preference.connection.ClientCertificatePreference.CLIENT_CERTIFICATE; import static org.ostrya.presencepublisher.ui.preference.connection.HostPreference.HOST; import static org.ostrya.presencepublisher.ui.preference.connection.PasswordPreference.PASSWORD; import static org.ostrya.presencepublisher.ui.preference.connection.PortPreference.PORT; import static org.ostrya.presencepublisher.ui.preference.connection.UseTlsPreference.USE_TLS; import static org.ostrya.presencepublisher.ui.preference.connection.UsernamePreference.USERNAME; import static org.ostrya.presencepublisher.ui.preference.schedule.AutostartPreference.AUTOSTART; import static org.ostrya.presencepublisher.ui.preference.schedule.BatteryTopicPreference.BATTERY_TOPIC; import static org.ostrya.presencepublisher.ui.preference.schedule.LastSuccessTimestampPreference.LAST_SUCCESS; import static org.ostrya.presencepublisher.ui.preference.schedule.MessageSchedulePreference.MESSAGE_SCHEDULE; import static org.ostrya.presencepublisher.ui.preference.schedule.NextScheduleTimestampPreference.NEXT_SCHEDULE; import static org.ostrya.presencepublisher.ui.preference.schedule.PresenceTopicPreference.PRESENCE_TOPIC; import static org.ostrya.presencepublisher.ui.preference.schedule.SendBatteryMessagePreference.SEND_BATTERY_MESSAGE; public class MainActivity extends FragmentActivity { private static final String TAG = "MainActivity"; private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceListener = this::onSharedPreferenceChanged; private boolean needsLocationService; private SharedPreferences sharedPreferences; @Override public void onCreate(Bundle savedInstanceState) { HyperLog.d(TAG, "Creating activity"); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MainPagerAdapter mainPagerAdapter = new MainPagerAdapter(getSupportFragmentManager(), getApplicationContext()); ViewPager viewPager = findViewById(R.id.pager); viewPager.setAdapter(mainPagerAdapter); needsLocationService = ((Application) getApplication()).supportsBeacons() // for WiFi name resolution || Build.VERSION.SDK_INT >= Build.VERSION_CODES.P; sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceListener); checkLocationPermissionAndAccessAndBluetoothAndBatteryOptimizationAndStartWorker(); HyperLog.d(TAG, "Creating activity finished"); } @Override protected void onResume() { super.onResume(); sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceListener); new Publisher(this).scheduleNow(); } @Override protected void onPause() { super.onPause(); sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceListener); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == PERMISSION_REQUEST_CODE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { HyperLog.i(TAG, "Successfully granted location permission"); checkLocationAccessAndBluetoothAndBatteryOptimizationAndStartWorker(); } } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == LOCATION_REQUEST_CODE) { // For some reason, the activity returns RESULT_CANCELED even when the service is enabled, so we don't // know if it was actually enabled or not. For now, we don't check again and just start the service. HyperLog.d(TAG, "Returning from location service with result " + resultCode + ", assuming it is running ..."); checkBluetoothAndBatteryOptimizationAndStartWorker(); } else if (requestCode == START_BLUETOOTH_REQUEST_CODE) { HyperLog.d(TAG, "Returning from bluetooth enabling with result " + resultCode); checkBatteryOptimizationAndStartWorker(); } else if (requestCode == BATTERY_OPTIMIZATION_REQUEST_CODE) { HyperLog.d(TAG, "Returning from battery optimization with result " + resultCode); new Publisher(this).scheduleNow(); } } private void onSharedPreferenceChanged(SharedPreferences preferences, String key) { switch (key) { case BEACON_LIST: case BATTERY_TOPIC: case CLIENT_CERTIFICATE: case HOST: case MESSAGE_SCHEDULE: case OFFLINE_CONTENT: case PASSWORD: case PORT: case PRESENCE_TOPIC: case SEND_BATTERY_MESSAGE: case SEND_OFFLINE_MESSAGE: case SEND_VIA_MOBILE_NETWORK: case SSID_LIST: case USERNAME: case USE_TLS: HyperLog.i(TAG, "Changed parameter " + key); new Publisher(this).scheduleNow(); break; case AUTOSTART: case LAST_SUCCESS: case NEXT_SCHEDULE: break; default: if (key.startsWith(WIFI_CONTENT_PREFIX)) { HyperLog.i(TAG, "Changed parameter " + key); new Publisher(this).scheduleNow(); } else { HyperLog.v(TAG, "Ignoring unexpected value " + key); } } } private void checkLocationPermissionAndAccessAndBluetoothAndBatteryOptimizationAndStartWorker() { if (needsLocationService && ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { HyperLog.d(TAG, "Location permission not yet granted, asking user ..."); FragmentManager fm = getSupportFragmentManager(); ConfirmationDialogFragment fragment = ConfirmationDialogFragment.getInstance(ok -> { if (ok) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION}, PERMISSION_REQUEST_CODE); } else { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_REQUEST_CODE); } } }, R.string.permission_dialog_title, R.string.permission_dialog_message); fragment.show(fm, null); } else { checkLocationAccessAndBluetoothAndBatteryOptimizationAndStartWorker(); } } private void checkLocationAccessAndBluetoothAndBatteryOptimizationAndStartWorker() { LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); if (needsLocationService && ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && (locationManager == null || !(locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)))) { HyperLog.d(TAG, "Location service not yet enabled, asking user ..."); FragmentManager fm = getSupportFragmentManager(); ConfirmationDialogFragment fragment = ConfirmationDialogFragment.getInstance(ok -> { if (ok) { startActivityForResult(new Intent(ACTION_LOCATION_SOURCE_SETTINGS), LOCATION_REQUEST_CODE); } }, R.string.location_dialog_title, R.string.location_dialog_message); fragment.show(fm, null); } else { checkBluetoothAndBatteryOptimizationAndStartWorker(); } } private void checkBluetoothAndBatteryOptimizationAndStartWorker() { if (((Application) getApplication()).supportsBeacons() // make linter happy && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && !sharedPreferences.getStringSet(BEACON_LIST, Collections.emptySet()).isEmpty()) { BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); if (bluetoothManager == null) { HyperLog.w(TAG, "Unable to get bluetooth manager"); } else { BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter(); if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { FragmentManager fm = getSupportFragmentManager(); ConfirmationDialogFragment fragment = ConfirmationDialogFragment.getInstance(ok -> { if (ok) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, START_BLUETOOTH_REQUEST_CODE); } }, R.string.bluetooth_dialog_title, R.string.bluetooth_dialog_message); fragment.show(fm, null); return; } } } checkBatteryOptimizationAndStartWorker(); } private void checkBatteryOptimizationAndStartWorker() { PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && powerManager != null && !powerManager.isIgnoringBatteryOptimizations(getPackageName())) { HyperLog.d(TAG, "Battery optimization not yet disabled, asking user ..."); FragmentManager fm = getSupportFragmentManager(); // this app should fall under "task automation app" in // https://developer.android.com/training/monitoring-device-state/doze-standby.html#whitelisting-cases @SuppressLint("BatteryLife") ConfirmationDialogFragment fragment = ConfirmationDialogFragment.getInstance(ok -> { if (ok) { Uri packageUri = Uri.fromParts("package", getPackageName(), null); startActivityForResult( new Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, packageUri), BATTERY_OPTIMIZATION_REQUEST_CODE); } }, R.string.battery_optimization_dialog_title, R.string.battery_optimization_dialog_message); fragment.show(fm, null); } else { new Publisher(this).scheduleNow(); } } }