115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes/*
215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes * Copyright (C) 2015 The Android Open Source Project
315ad53853d367f5d593bb019d88fb613878fd8fcChris Banes *
415ad53853d367f5d593bb019d88fb613878fd8fcChris Banes * Licensed under the Apache License, Version 2.0 (the "License");
515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes * you may not use this file except in compliance with the License.
615ad53853d367f5d593bb019d88fb613878fd8fcChris Banes * You may obtain a copy of the License at
715ad53853d367f5d593bb019d88fb613878fd8fcChris Banes *
815ad53853d367f5d593bb019d88fb613878fd8fcChris Banes *      http://www.apache.org/licenses/LICENSE-2.0
915ad53853d367f5d593bb019d88fb613878fd8fcChris Banes *
1015ad53853d367f5d593bb019d88fb613878fd8fcChris Banes * Unless required by applicable law or agreed to in writing, software
1115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes * distributed under the License is distributed on an "AS IS" BASIS,
1215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1315ad53853d367f5d593bb019d88fb613878fd8fcChris Banes * See the License for the specific language governing permissions and
1415ad53853d367f5d593bb019d88fb613878fd8fcChris Banes * limitations under the License.
1515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes */
1615ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
1715ad53853d367f5d593bb019d88fb613878fd8fcChris Banespackage android.support.v7.app;
1815ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
1950b45b25e10ddbfd726b91910e00bce6a1c63904Aurimas Liutikasimport static android.Manifest.permission.ACCESS_COARSE_LOCATION;
2050b45b25e10ddbfd726b91910e00bce6a1c63904Aurimas Liutikasimport static android.Manifest.permission.ACCESS_FINE_LOCATION;
2150b45b25e10ddbfd726b91910e00bce6a1c63904Aurimas Liutikas
22f0ec407dc5fdb420550ed29f9556909bd9f635a9Chris Banesimport android.Manifest;
2350b45b25e10ddbfd726b91910e00bce6a1c63904Aurimas Liutikasimport android.annotation.SuppressLint;
2415ad53853d367f5d593bb019d88fb613878fd8fcChris Banesimport android.content.Context;
2515ad53853d367f5d593bb019d88fb613878fd8fcChris Banesimport android.location.Location;
2615ad53853d367f5d593bb019d88fb613878fd8fcChris Banesimport android.location.LocationManager;
2715ad53853d367f5d593bb019d88fb613878fd8fcChris Banesimport android.support.annotation.NonNull;
2850b45b25e10ddbfd726b91910e00bce6a1c63904Aurimas Liutikasimport android.support.annotation.RequiresPermission;
2912acd8f6d6926aaa5cc16543386e0aeee64716edChris Banesimport android.support.annotation.VisibleForTesting;
30f0ec407dc5fdb420550ed29f9556909bd9f635a9Chris Banesimport android.support.v4.content.PermissionChecker;
3115ad53853d367f5d593bb019d88fb613878fd8fcChris Banesimport android.text.format.DateUtils;
3215ad53853d367f5d593bb019d88fb613878fd8fcChris Banesimport android.util.Log;
3315ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
3415ad53853d367f5d593bb019d88fb613878fd8fcChris Banesimport java.util.Calendar;
3515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
3615ad53853d367f5d593bb019d88fb613878fd8fcChris Banes/**
3715ad53853d367f5d593bb019d88fb613878fd8fcChris Banes * Class which managing whether we are in the night or not.
3815ad53853d367f5d593bb019d88fb613878fd8fcChris Banes */
3915ad53853d367f5d593bb019d88fb613878fd8fcChris Banesclass TwilightManager {
4015ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
4115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    private static final String TAG = "TwilightManager";
4215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
4315ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    private static final int SUNRISE = 6; // 6am
4415ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    private static final int SUNSET = 22; // 10pm
4515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
4612acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes    private static TwilightManager sInstance;
4712acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes
4812acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes    static TwilightManager getInstance(@NonNull Context context) {
4912acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes        if (sInstance == null) {
5012acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes            context = context.getApplicationContext();
5112acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes            sInstance = new TwilightManager(context,
5212acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes                    (LocationManager) context.getSystemService(Context.LOCATION_SERVICE));
5312acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes        }
5412acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes        return sInstance;
5512acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes    }
5612acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes
5712acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes    @VisibleForTesting
5812acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes    static void setInstance(TwilightManager twilightManager) {
5912acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes        sInstance = twilightManager;
6012acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes    }
6115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
6215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    private final Context mContext;
6315ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    private final LocationManager mLocationManager;
6415ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
6512acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes    private final TwilightState mTwilightState = new TwilightState();
6612acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes
6712acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes    @VisibleForTesting
6812acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes    TwilightManager(@NonNull Context context, @NonNull LocationManager locationManager) {
6915ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        mContext = context;
7012acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes        mLocationManager = locationManager;
7115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    }
7215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
7315ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    /**
7415ad53853d367f5d593bb019d88fb613878fd8fcChris Banes     * Returns true we are currently in the 'night'.
7515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes     *
7615ad53853d367f5d593bb019d88fb613878fd8fcChris Banes     * @return true if we are at night, false if the day.
7715ad53853d367f5d593bb019d88fb613878fd8fcChris Banes     */
7815ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    boolean isNight() {
7912acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes        final TwilightState state = mTwilightState;
8015ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
8112acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes        if (isStateValid()) {
8215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            // If the current twilight state is still valid, use it
8315ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            return state.isNight;
8415ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        }
8515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
8615ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        // Else, we will try and grab the last known location
8715ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        final Location location = getLastKnownLocation();
8815ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        if (location != null) {
8915ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            updateState(location);
9015ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            return state.isNight;
9115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        }
9215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
9315ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        Log.i(TAG, "Could not get last known location. This is probably because the app does not"
9415ad53853d367f5d593bb019d88fb613878fd8fcChris Banes                + " have any location permissions. Falling back to hardcoded"
9515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes                + " sunrise/sunset values.");
9615ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
9715ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        // If we don't have a location, we'll use our hardcoded sunrise/sunset values.
9815ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        // These aren't great, but it's better than nothing.
9915ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        Calendar calendar = Calendar.getInstance();
10015ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        final int hour = calendar.get(Calendar.HOUR_OF_DAY);
10115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        return hour < SUNRISE || hour >= SUNSET;
10215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    }
10315ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
10450b45b25e10ddbfd726b91910e00bce6a1c63904Aurimas Liutikas    @SuppressLint("MissingPermission") // permissions are checked for the needed call.
10515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    private Location getLastKnownLocation() {
1060ceb9e98ba7ba67346e0dab85384560a23740fd0Chris Banes        Location coarseLoc = null;
1070ceb9e98ba7ba67346e0dab85384560a23740fd0Chris Banes        Location fineLoc = null;
108f0ec407dc5fdb420550ed29f9556909bd9f635a9Chris Banes
109f0ec407dc5fdb420550ed29f9556909bd9f635a9Chris Banes        int permission = PermissionChecker.checkSelfPermission(mContext,
1100ceb9e98ba7ba67346e0dab85384560a23740fd0Chris Banes                Manifest.permission.ACCESS_COARSE_LOCATION);
111f0ec407dc5fdb420550ed29f9556909bd9f635a9Chris Banes        if (permission == PermissionChecker.PERMISSION_GRANTED) {
1120ceb9e98ba7ba67346e0dab85384560a23740fd0Chris Banes            coarseLoc = getLastKnownLocationForProvider(LocationManager.NETWORK_PROVIDER);
113f0ec407dc5fdb420550ed29f9556909bd9f635a9Chris Banes        }
114f0ec407dc5fdb420550ed29f9556909bd9f635a9Chris Banes
115f0ec407dc5fdb420550ed29f9556909bd9f635a9Chris Banes        permission = PermissionChecker.checkSelfPermission(mContext,
1160ceb9e98ba7ba67346e0dab85384560a23740fd0Chris Banes                Manifest.permission.ACCESS_FINE_LOCATION);
117f0ec407dc5fdb420550ed29f9556909bd9f635a9Chris Banes        if (permission == PermissionChecker.PERMISSION_GRANTED) {
1180ceb9e98ba7ba67346e0dab85384560a23740fd0Chris Banes            fineLoc = getLastKnownLocationForProvider(LocationManager.GPS_PROVIDER);
119f0ec407dc5fdb420550ed29f9556909bd9f635a9Chris Banes        }
12015ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
1210ceb9e98ba7ba67346e0dab85384560a23740fd0Chris Banes        if (fineLoc != null && coarseLoc != null) {
12215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            // If we have both a fine and coarse location, use the latest
1230ceb9e98ba7ba67346e0dab85384560a23740fd0Chris Banes            return fineLoc.getTime() > coarseLoc.getTime() ? fineLoc : coarseLoc;
12415ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        } else {
12515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            // Else, return the non-null one (if there is one)
1260ceb9e98ba7ba67346e0dab85384560a23740fd0Chris Banes            return fineLoc != null ? fineLoc : coarseLoc;
12715ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        }
12815ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    }
12915ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
13050b45b25e10ddbfd726b91910e00bce6a1c63904Aurimas Liutikas    @RequiresPermission(anyOf = {ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION})
13115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    private Location getLastKnownLocationForProvider(String provider) {
13250b45b25e10ddbfd726b91910e00bce6a1c63904Aurimas Liutikas        try {
13350b45b25e10ddbfd726b91910e00bce6a1c63904Aurimas Liutikas            if (mLocationManager.isProviderEnabled(provider)) {
13450b45b25e10ddbfd726b91910e00bce6a1c63904Aurimas Liutikas                return mLocationManager.getLastKnownLocation(provider);
13515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            }
13650b45b25e10ddbfd726b91910e00bce6a1c63904Aurimas Liutikas        } catch (Exception e) {
13750b45b25e10ddbfd726b91910e00bce6a1c63904Aurimas Liutikas            Log.d(TAG, "Failed to get last known location", e);
13815ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        }
13915ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        return null;
14015ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    }
14115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
14212acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes    private boolean isStateValid() {
14350b45b25e10ddbfd726b91910e00bce6a1c63904Aurimas Liutikas        return mTwilightState.nextUpdate > System.currentTimeMillis();
14415ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    }
14515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
14615ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    private void updateState(@NonNull Location location) {
14712acd8f6d6926aaa5cc16543386e0aeee64716edChris Banes        final TwilightState state = mTwilightState;
14815ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        final long now = System.currentTimeMillis();
14915ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        final TwilightCalculator calculator = TwilightCalculator.getInstance();
15015ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
15115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        // calculate yesterday's twilight
15215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        calculator.calculateTwilight(now - DateUtils.DAY_IN_MILLIS,
15315ad53853d367f5d593bb019d88fb613878fd8fcChris Banes                location.getLatitude(), location.getLongitude());
15415ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        final long yesterdaySunset = calculator.sunset;
15515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
15615ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        // calculate today's twilight
15715ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        calculator.calculateTwilight(now, location.getLatitude(), location.getLongitude());
15815ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        final boolean isNight = (calculator.state == TwilightCalculator.NIGHT);
15915ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        final long todaySunrise = calculator.sunrise;
16015ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        final long todaySunset = calculator.sunset;
16115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
16215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        // calculate tomorrow's twilight
16315ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        calculator.calculateTwilight(now + DateUtils.DAY_IN_MILLIS,
16415ad53853d367f5d593bb019d88fb613878fd8fcChris Banes                location.getLatitude(), location.getLongitude());
16515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        final long tomorrowSunrise = calculator.sunrise;
16615ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
16715ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        // Set next update
16815ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        long nextUpdate = 0;
16915ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        if (todaySunrise == -1 || todaySunset == -1) {
17015ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            // In the case the day or night never ends the update is scheduled 12 hours later.
17115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            nextUpdate = now + 12 * DateUtils.HOUR_IN_MILLIS;
17215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        } else {
17315ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            if (now > todaySunset) {
17415ad53853d367f5d593bb019d88fb613878fd8fcChris Banes                nextUpdate += tomorrowSunrise;
17515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            } else if (now > todaySunrise) {
17615ad53853d367f5d593bb019d88fb613878fd8fcChris Banes                nextUpdate += todaySunset;
17715ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            } else {
17815ad53853d367f5d593bb019d88fb613878fd8fcChris Banes                nextUpdate += todaySunrise;
17915ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            }
18015ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            // add some extra time to be on the safe side.
18115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes            nextUpdate += DateUtils.MINUTE_IN_MILLIS;
18215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        }
18315ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
18415ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        // Update the twilight state
18515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        state.isNight = isNight;
18615ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        state.yesterdaySunset = yesterdaySunset;
18715ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        state.todaySunrise = todaySunrise;
18815ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        state.todaySunset = todaySunset;
18915ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        state.tomorrowSunrise = tomorrowSunrise;
19015ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        state.nextUpdate = nextUpdate;
19115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    }
19215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes
19315ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    /**
19415ad53853d367f5d593bb019d88fb613878fd8fcChris Banes     * Describes whether it is day or night.
19515ad53853d367f5d593bb019d88fb613878fd8fcChris Banes     */
19615ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    private static class TwilightState {
19715ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        boolean isNight;
19815ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        long yesterdaySunset;
19915ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        long todaySunrise;
20015ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        long todaySunset;
20115ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        long tomorrowSunrise;
20215ad53853d367f5d593bb019d88fb613878fd8fcChris Banes        long nextUpdate;
2032c1bad7ecd5879bf0f29ce2ce1bc5bd67a3f4682Aurimas Liutikas
2042c1bad7ecd5879bf0f29ce2ce1bc5bd67a3f4682Aurimas Liutikas        TwilightState() {
2052c1bad7ecd5879bf0f29ce2ce1bc5bd67a3f4682Aurimas Liutikas        }
20615ad53853d367f5d593bb019d88fb613878fd8fcChris Banes    }
20715ad53853d367f5d593bb019d88fb613878fd8fcChris Banes}
208