package sk.henrichg.phoneprofilesplus; import android.annotation.SuppressLint; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.location.Criteria; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.SystemClock; import android.text.format.DateUtils; import android.text.format.Time; import androidx.work.Data; import androidx.work.ExistingWorkPolicy; import androidx.work.OneTimeWorkRequest; import androidx.work.WorkManager; import java.util.Calendar; import java.util.Iterator; import java.util.concurrent.TimeUnit; class TwilightScanner { //private static final String TAG = "TwilightScanner"; //private static final boolean DEBUG = false; private static final String ACTION_UPDATE_TWILIGHT_STATE = PPApplication.PACKAGE_NAME + ".TwilightScanner.ACTION_UPDATE_TWILIGHT_STATE"; private final Object mLock = new Object(); private final Context context; private final AlarmManager mAlarmManager; private final LocationManager mLocationManager; private final LocationHandler mLocationHandler; private TwilightState mTwilightState; TwilightScanner(Context context) { this.context = context; mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); mLocationHandler = new LocationHandler(); } void start() { IntentFilter filter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED); filter.addAction(Intent.ACTION_TIME_CHANGED); filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); filter.addAction(TwilightScanner.ACTION_UPDATE_TWILIGHT_STATE); context.registerReceiver(mUpdateLocationReceiver, filter); mLocationHandler.enableLocationUpdates(); } void stop() { mLocationHandler.requestLocationUpdate(); try { context.unregisterReceiver(mUpdateLocationReceiver); } catch (Exception e) { //PPApplication.recordException(e); } } private void setTwilightState(TwilightState state) { synchronized (mLock) { if ((mTwilightState == null) || (state == null) || !mTwilightState.equals(state)) { //PPApplication.logE("TwilightScanner.setTwilightState", "Twilight state changed: " + state); mTwilightState = state; //final Context appContext = context.getApplicationContext(); if (!PPApplication.getApplicationStarted(true)) // application is not started return; if (Event.getGlobalEventsRunning()) { //PPApplication.logE("TwilightScanner.setTwilightState", "xxx"); Data workData = new Data.Builder() .putString(PhoneProfilesService.EXTRA_DELAYED_WORK, DelayedWorksWorker.DELAYED_WORK_HANDLE_EVENTS) .putString(PhoneProfilesService.EXTRA_SENSOR_TYPE, EventsHandler.SENSOR_TYPE_TIME) .build(); OneTimeWorkRequest worker = new OneTimeWorkRequest.Builder(DelayedWorksWorker.class) .addTag(DelayedWorksWorker.DELAYED_WORK_HANDLE_EVENTS_TWILIGHT_SCANNER_WORK_TAG) .setInputData(workData) .setInitialDelay(10, TimeUnit.SECONDS) // 10 seconds to get location .build(); try { if (PPApplication.getApplicationStarted(true)) { WorkManager workManager = PPApplication.getWorkManagerInstance(); if (workManager != null) workManager.enqueueUniqueWork(DelayedWorksWorker.DELAYED_WORK_HANDLE_EVENTS_TWILIGHT_SCANNER_WORK_TAG, ExistingWorkPolicy.KEEP, worker); //workManager.enqueue(worker); } } catch (Exception e) { PPApplication.recordException(e); } /* PPApplication.startHandlerThread();//"TwilightScanner.setTwilightState" final Handler handler = new Handler(PPApplication.handlerThread.getLooper()); handler.post(new Runnable() { @Override public void run() { PowerManager powerManager = (PowerManager) appContext.getSystemService(Context.POWER_SERVICE); PowerManager.WakeLock wakeLock = null; try { if (powerManager != null) { wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, PPApplication.PACKAGE_NAME + ":TwilightScanner_setTwilightState"); wakeLock.acquire(10 * 60 * 1000); } PPApplication.logE("****** EventsHandler.handleEvents", "START run - from=TwilightScanner.setTwilightState"); EventsHandler eventsHandler = new EventsHandler(appContext); eventsHandler.handleEvents(EventsHandler.SENSOR_TYPE_TIME); PPApplication.logE("****** EventsHandler.handleEvents", "END run - from=TwilightScanner.setTwilightState"); } finally { if ((wakeLock != null) && wakeLock.isHeld()) { try { wakeLock.release(); } catch (Exception ignored) { } } } } }); */ } } } } TwilightState getTwilightState(boolean log) { //if (log) // PPApplication.logE("TwilightScanner.getTwilightState", "xxx"); synchronized (mLock) { mLocationHandler.updateTwilightState(false, log); //if (log) // PPApplication.logE("TwilightScanner.getTwilightState", "END"); return mTwilightState; } } // The user has moved if the accuracy circles of the two locations don't overlap. private static boolean hasMoved(Location from, Location to) { if (to == null) { return false; } if (from == null) { return true; } // if new location is older than the current one, the device hasn't moved. if (to.getElapsedRealtimeNanos() < from.getElapsedRealtimeNanos()) { return false; } // Get the distance between the two points. float distance = from.distanceTo(to); // Get the total accuracy radius for both locations. float totalAccuracy = from.getAccuracy() + to.getAccuracy(); // If the distance is greater than the combined accuracy of the two // points then they can't overlap and hence the user has moved. return distance >= totalAccuracy; } @SuppressLint("HandlerLeak") private final class LocationHandler extends Handler { private static final int MSG_ENABLE_LOCATION_UPDATES = 1; private static final int MSG_GET_NEW_LOCATION_UPDATE = 2; private static final int MSG_PROCESS_NEW_LOCATION = 3; private static final int MSG_DO_TWILIGHT_UPDATE = 4; private static final long LOCATION_UPDATE_MS = 24 * DateUtils.HOUR_IN_MILLIS; private static final long MIN_LOCATION_UPDATE_MS = 30 * DateUtils.MINUTE_IN_MILLIS; private static final float LOCATION_UPDATE_DISTANCE_METER = 1000 * 20; private static final long LOCATION_UPDATE_ENABLE_INTERVAL_MIN = 5000; private static final long LOCATION_UPDATE_ENABLE_INTERVAL_MAX = 15 * DateUtils.MINUTE_IN_MILLIS; private static final double FACTOR_GMT_OFFSET_LONGITUDE = 1000.0 * 360.0 / DateUtils.DAY_IN_MILLIS; private boolean mPassiveListenerEnabled; private boolean mNetworkListenerEnabled; private boolean mDidFirstInit; private long mLastNetworkRegisterTime = -MIN_LOCATION_UPDATE_MS; private long mLastUpdateInterval; private Location mLocation; void processNewLocation(Location location) { Message msg = obtainMessage(MSG_PROCESS_NEW_LOCATION, location); sendMessage(msg); } void enableLocationUpdates() { sendEmptyMessage(MSG_ENABLE_LOCATION_UPDATES); } void requestLocationUpdate() { sendEmptyMessage(MSG_GET_NEW_LOCATION_UPDATE); } void requestTwilightUpdate() { sendEmptyMessage(MSG_DO_TWILIGHT_UPDATE); } @SuppressLint("MissingPermission") @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_PROCESS_NEW_LOCATION: { final Location location = (Location) msg.obj; final boolean hasMoved = hasMoved(mLocation, location); final boolean hasBetterAccuracy = mLocation == null || location.getAccuracy() < mLocation.getAccuracy(); /*PPApplication.logE("TwilightScanner.handleMessage", "Processing new location: " + location + ", hasMoved=" + hasMoved + ", hasBetterAccuracy=" + hasBetterAccuracy);*/ if (hasMoved || hasBetterAccuracy) { setLocation(location); } break; } case MSG_GET_NEW_LOCATION_UPDATE: //PPApplication.logE("TwilightScanner.handleMessage", "MSG_GET_NEW_LOCATION_UPDATE"); if (!mNetworkListenerEnabled) { // Don't do anything -- we are still trying to get a // location. return; } if ((mLastNetworkRegisterTime + MIN_LOCATION_UPDATE_MS) >= SystemClock.elapsedRealtime()) { // Don't do anything -- it hasn't been long enough // since we last requested an update. return; } // Unregister the current location monitor, so we can // register a new one for it to get an immediate update. mNetworkListenerEnabled = false; if (mLocationManager != null) mLocationManager.removeUpdates(mEmptyLocationListener); // Fall through to re-register listener. case MSG_ENABLE_LOCATION_UPDATES: // enable network provider to receive at least location updates for a given // distance. //PPApplication.logE("TwilightScanner.handleMessage", "MSG_ENABLE_LOCATION_UPDATES"); boolean networkLocationEnabled; try { //noinspection ConstantConditions networkLocationEnabled = mLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER); } catch (Exception e) { // we may get IllegalArgumentException if network location provider // does not exist or is not yet installed. networkLocationEnabled = false; } if (!mNetworkListenerEnabled && networkLocationEnabled) { if (Permissions.checkLocation(context)) { mNetworkListenerEnabled = true; mLastNetworkRegisterTime = SystemClock.elapsedRealtime(); mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, LOCATION_UPDATE_MS, 0, mEmptyLocationListener); if (!mDidFirstInit) { mDidFirstInit = true; if (mLocation == null) { retrieveLocation(); } } } } // enable passive provider to receive updates from location fixes (gps // and network). boolean passiveLocationEnabled; try { passiveLocationEnabled = mLocationManager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER); } catch (Exception e) { // we may get IllegalArgumentException if passive location provider // does not exist or is not yet installed. passiveLocationEnabled = false; } if (!mPassiveListenerEnabled && passiveLocationEnabled) { if (Permissions.checkLocation(context)) { mPassiveListenerEnabled = true; mLocationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, LOCATION_UPDATE_DISTANCE_METER, mLocationListener); } } if (!(mNetworkListenerEnabled && mPassiveListenerEnabled)) { mLastUpdateInterval *= 1.5; if (mLastUpdateInterval == 0) { mLastUpdateInterval = LOCATION_UPDATE_ENABLE_INTERVAL_MIN; } else if (mLastUpdateInterval > LOCATION_UPDATE_ENABLE_INTERVAL_MAX) { mLastUpdateInterval = LOCATION_UPDATE_ENABLE_INTERVAL_MAX; } sendEmptyMessageDelayed(MSG_ENABLE_LOCATION_UPDATES, mLastUpdateInterval); } break; case MSG_DO_TWILIGHT_UPDATE: //PPApplication.logE("TwilightScanner.handleMessage", "MSG_DO_TWILIGHT_UPDATE"); updateTwilightState(true, true); break; } } private void retrieveLocation() { Location location = null; if (Permissions.checkLocation(context)) { if (mLocationManager != null) { final Iterator<String> providers = mLocationManager.getProviders(new Criteria(), true).iterator(); //noinspection WhileLoopReplaceableByForEach while (providers.hasNext()) { @SuppressLint("MissingPermission") final Location lastKnownLocation = mLocationManager.getLastKnownLocation(providers.next()); // pick the most recent location if (location == null || (lastKnownLocation != null && location.getElapsedRealtimeNanos() < lastKnownLocation.getElapsedRealtimeNanos())) { location = lastKnownLocation; } } } } // In the case there is no location available (e.g. GPS fix or network location // is not available yet), the longitude of the location is estimated using the timezone, // latitude and accuracy are set to get a good average. if (location == null) { //noinspection deprecation Time currentTime = new Time(); currentTime.set(System.currentTimeMillis()); double lngOffset = FACTOR_GMT_OFFSET_LONGITUDE * (currentTime.gmtoff - (currentTime.isDst > 0 ? 3600 : 0)); location = new Location("fake"); location.setLongitude(lngOffset); location.setLatitude(0); location.setAccuracy(417000.0f); location.setTime(System.currentTimeMillis()); location.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos()); //PPApplication.logE("TwilightScanner.retrieveLocation", "Estimated location from timezone: " + location); } setLocation(location); } private void setLocation(Location location) { //PPApplication.logE("TwilightScanner.setLocation", "xxx"); mLocation = location; updateTwilightState(true, true); } void updateTwilightState(boolean setAlarm, @SuppressWarnings("unused") boolean log) { if (mLocation == null) { setTwilightState(null); return; } Calendar now = Calendar.getInstance(); /*if (log) PPApplication.logE("TwilightScanner.updateTwilightState", "now=" + now.getTime());*/ // Calendar[] twilight = SunriseSunset.getSunriseSunset(Calendar.getInstance(), mLocation.getLatitude(), mLocation.getLongitude()); // PPApplication.logE("TwilightScanner.updateTwilightState", "SunriseSunset.getCivilTwilight[0]=" + twilight[0].getTime()); // PPApplication.logE("TwilightScanner.updateTwilightState", "SunriseSunset.getCivilTwilight[1]=" + twilight[1].getTime()); // calculate correction Calendar[] twilight = SunriseSunset.getSunriseSunset(now, mLocation.getLatitude(), mLocation.getLongitude()); // check if must be applied correction Calendar _now = Calendar.getInstance(); _now.set(Calendar.HOUR_OF_DAY, 0); _now.set(Calendar.MINUTE, 0); _now.set(Calendar.SECOND, 0); _now.set(Calendar.MILLISECOND, 0); Calendar _twilight = twilight[0]; _twilight.set(Calendar.HOUR_OF_DAY, 0); _twilight.set(Calendar.MINUTE, 0); _twilight.set(Calendar.SECOND, 0); _twilight.set(Calendar.MILLISECOND, 0); int correction = -_twilight.compareTo(_now); /*if (log) { if (PPApplication.logEnabled()) { PPApplication.logE("TwilightScanner.updateTwilightState", "_now=" + _now.getTime()); PPApplication.logE("TwilightScanner.updateTwilightState", "_twilight=" + _twilight.getTime()); PPApplication.logE("TwilightScanner.updateTwilightState", "correction=" + correction); } }*/ // calculate today twilight _now = Calendar.getInstance(); _now.add(Calendar.DAY_OF_YEAR, correction); twilight = SunriseSunset.getSunriseSunset(_now, mLocation.getLatitude(), mLocation.getLongitude()); boolean isNight = SunriseSunset.isNight(_now, mLocation.getLatitude(), mLocation.getLongitude()); long todaySunrise = twilight[0].getTimeInMillis(); long todaySunset = twilight[1].getTimeInMillis(); // calculate yesterday's twilight Calendar yesterday = Calendar.getInstance(); yesterday.add(Calendar.DAY_OF_YEAR, -1 + correction); twilight = SunriseSunset.getSunriseSunset(yesterday, mLocation.getLatitude(), mLocation.getLongitude()); final long yesterdaySunrise = twilight[0].getTimeInMillis(); final long yesterdaySunset = twilight[1].getTimeInMillis(); // calculate tomorrow's twilight Calendar tomorrow = Calendar.getInstance(); tomorrow.add(Calendar.DAY_OF_YEAR, 1 + correction); twilight = SunriseSunset.getSunriseSunset(tomorrow, mLocation.getLatitude(), mLocation.getLongitude()); final long tomorrowSunrise = twilight[0].getTimeInMillis(); final long tomorrowSunset = twilight[1].getTimeInMillis(); long[] daysSunrise = new long[9]; long[] daysSunset = new long[9]; Calendar day = Calendar.getInstance(); day.add(Calendar.DAY_OF_YEAR, -1 + correction); // index = 0 -> yesterday, for (int i = 0; i < 9; i++) { twilight = SunriseSunset.getSunriseSunset(day, mLocation.getLatitude(), mLocation.getLongitude()); daysSunrise[i] = twilight[0].getTimeInMillis(); daysSunset[i] = twilight[1].getTimeInMillis(); day.add(Calendar.DAY_OF_YEAR, 1); } // set twilight state TwilightState state = new TwilightState(isNight, yesterdaySunrise, yesterdaySunset, todaySunrise, todaySunset, tomorrowSunrise, tomorrowSunset, daysSunrise, daysSunset); //if (log) // PPApplication.logE("TwilightScanner.updateTwilightState", "Updating twilight state: " + state); setTwilightState(state); if (setAlarm) { // schedule next update long nextUpdate = 0; if (todaySunrise == -1 || todaySunset == -1) { // In the case the day or night never ends the update is scheduled 12 hours later. nextUpdate = now.getTimeInMillis() + 12 * DateUtils.HOUR_IN_MILLIS; } else { // add some extra time to be on the safe side. nextUpdate += DateUtils.MINUTE_IN_MILLIS; if (now.getTimeInMillis() > todaySunset) { nextUpdate += tomorrowSunrise; } else if (now.getTimeInMillis() > todaySunrise) { nextUpdate += todaySunset; } else { nextUpdate += todaySunrise; } } // remove alarm try { Intent updateIntent = new Intent(ACTION_UPDATE_TWILIGHT_STATE); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, updateIntent, PendingIntent.FLAG_NO_CREATE); if (pendingIntent != null) { if (mAlarmManager != null) mAlarmManager.cancel(pendingIntent); pendingIntent.cancel(); } } catch (Exception e) { PPApplication.recordException(e); } PhoneProfilesService.cancelWork(ElapsedAlarmsWorker.ELAPSED_ALARMS_TWILIGHT_SCANNER_TAG_WORK); // set alarm /*if (ApplicationPreferences.applicationUseAlarmClock(context)) { Intent updateIntent = new Intent(ACTION_UPDATE_TWILIGHT_STATE); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, updateIntent, PendingIntent.FLAG_UPDATE_CURRENT); if (log) PPApplication.logE("TwilightScanner.updateTwilightState", "nextUpdate=" + DateFormat.getDateFormat(context).format(nextUpdate) + " " + DateFormat.getTimeFormat(context).format(nextUpdate)); Intent editorIntent = new Intent(context, EditorProfilesActivity.class); editorIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent infoPendingIntent = PendingIntent.getActivity(context, 1000, editorIntent, PendingIntent.FLAG_UPDATE_CURRENT); AlarmManager.AlarmClockInfo clockInfo = new AlarmManager.AlarmClockInfo(nextUpdate, infoPendingIntent); mAlarmManager.setAlarmClock(clockInfo, pendingIntent); } else { now = Calendar.getInstance(); long elapsedTime = nextUpdate - now.getTimeInMillis(); if (log) { if (PPApplication.logEnabled()) { long allSeconds = elapsedTime / 1000; long hours = allSeconds / 60 / 60; long minutes = (allSeconds - (hours * 60 * 60)) / 60; long seconds = allSeconds % 60; PPApplication.logE("TwilightScanner.updateTwilightState", "elapsedTime=" + hours + ":" + minutes + ":" + seconds); } } Data workData = new Data.Builder() .putString(PhoneProfilesService.EXTRA_ELAPSED_ALARMS_WORK, ElapsedAlarmsWorker.ELAPSED_ALARMS_TWILIGHT_SCANNER) .build(); OneTimeWorkRequest worker = new OneTimeWorkRequest.Builder(ElapsedAlarmsWorker.class) .addTag("elapsedAlarmsTwilightScannerWork") .setInputData(workData) .setInitialDelay(elapsedTime, TimeUnit.MILLISECONDS) .build(); try { WorkManager workManager = WorkManager.getInstance(context); if (log) PPApplication.logE("[HANDLER] TwilightScanner.updateTwilightState", "enqueueUniqueWork - elapsedTime="+elapsedTime); workManager.enqueueUniqueWork("elapsedAlarmsTwilightScannerWork", ExistingWorkPolicy.KEEP, worker); } catch (Exception ignored) {} }*/ Intent updateIntent = new Intent(ACTION_UPDATE_TWILIGHT_STATE); if (mAlarmManager != null) { PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, updateIntent, PendingIntent.FLAG_NO_CREATE); if (pendingIntent != null) { //PPApplication.logE("EventPreferencesSMS.removeAlarm", "alarm found"); mAlarmManager.cancel(pendingIntent); pendingIntent.cancel(); } pendingIntent = PendingIntent.getBroadcast(context, 0, updateIntent, PendingIntent.FLAG_UPDATE_CURRENT); if (ApplicationPreferences.applicationUseAlarmClock) { Intent editorIntent = new Intent(context, EditorProfilesActivity.class); editorIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent infoPendingIntent = PendingIntent.getActivity(context, 1000, editorIntent, PendingIntent.FLAG_UPDATE_CURRENT); AlarmManager.AlarmClockInfo clockInfo = new AlarmManager.AlarmClockInfo(nextUpdate, infoPendingIntent); mAlarmManager.setAlarmClock(clockInfo, pendingIntent); } else { //if (android.os.Build.VERSION.SDK_INT >= 23) mAlarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, nextUpdate, pendingIntent); //else // mAlarmManager.setExact(AlarmManager.RTC_WAKEUP, nextUpdate, pendingIntent); } } } } } private final BroadcastReceiver mUpdateLocationReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { //PPApplication.logE("TwilightScanner.mUpdateLocationReceiver", "xxx"); if (Intent.ACTION_AIRPLANE_MODE_CHANGED.equals(intent.getAction()) && !intent.getBooleanExtra("state", false)) { // Airplane mode is now off! mLocationHandler.requestLocationUpdate(); return; } // Time zone has changed or alarm expired. mLocationHandler.requestTwilightUpdate(); } }; static void doWork() { //PPApplication.logE("TwilightScanner.doWork", "xxx"); if (PhoneProfilesService.getInstance() != null) { TwilightScanner twilightScanner = PhoneProfilesService.getInstance().getTwilightScanner(); if (twilightScanner != null) { // Time zone has changed or alarm expired. twilightScanner.mLocationHandler.requestTwilightUpdate(); } } } // A LocationListener to initialize the network location provider. The location updates // are handled through the passive location provider. private final LocationListener mEmptyLocationListener = new LocationListener() { public void onLocationChanged(Location location) { } public void onProviderDisabled(String provider) { } public void onProviderEnabled(String provider) { } public void onStatusChanged(String provider, int status, Bundle extras) { } }; private final LocationListener mLocationListener = new LocationListener() { public void onLocationChanged(Location location) { mLocationHandler.processNewLocation(location); } public void onProviderDisabled(String provider) { } public void onProviderEnabled(String provider) { } public void onStatusChanged(String provider, int status, Bundle extras) { } }; }