1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v7.app;
18
19import android.Manifest;
20import android.content.Context;
21import android.location.Location;
22import android.location.LocationManager;
23import android.support.annotation.NonNull;
24import android.support.annotation.VisibleForTesting;
25import android.support.v4.content.PermissionChecker;
26import android.text.format.DateUtils;
27import android.util.Log;
28
29import java.util.Calendar;
30
31/**
32 * Class which managing whether we are in the night or not.
33 */
34class TwilightManager {
35
36    private static final String TAG = "TwilightManager";
37
38    private static final int SUNRISE = 6; // 6am
39    private static final int SUNSET = 22; // 10pm
40
41    private static TwilightManager sInstance;
42
43    static TwilightManager getInstance(@NonNull Context context) {
44        if (sInstance == null) {
45            context = context.getApplicationContext();
46            sInstance = new TwilightManager(context,
47                    (LocationManager) context.getSystemService(Context.LOCATION_SERVICE));
48        }
49        return sInstance;
50    }
51
52    @VisibleForTesting
53    static void setInstance(TwilightManager twilightManager) {
54        sInstance = twilightManager;
55    }
56
57    private final Context mContext;
58    private final LocationManager mLocationManager;
59
60    private final TwilightState mTwilightState = new TwilightState();
61
62    @VisibleForTesting
63    TwilightManager(@NonNull Context context, @NonNull LocationManager locationManager) {
64        mContext = context;
65        mLocationManager = locationManager;
66    }
67
68    /**
69     * Returns true we are currently in the 'night'.
70     *
71     * @return true if we are at night, false if the day.
72     */
73    boolean isNight() {
74        final TwilightState state = mTwilightState;
75
76        if (isStateValid()) {
77            // If the current twilight state is still valid, use it
78            return state.isNight;
79        }
80
81        // Else, we will try and grab the last known location
82        final Location location = getLastKnownLocation();
83        if (location != null) {
84            updateState(location);
85            return state.isNight;
86        }
87
88        Log.i(TAG, "Could not get last known location. This is probably because the app does not"
89                + " have any location permissions. Falling back to hardcoded"
90                + " sunrise/sunset values.");
91
92        // If we don't have a location, we'll use our hardcoded sunrise/sunset values.
93        // These aren't great, but it's better than nothing.
94        Calendar calendar = Calendar.getInstance();
95        final int hour = calendar.get(Calendar.HOUR_OF_DAY);
96        return hour < SUNRISE || hour >= SUNSET;
97    }
98
99    private Location getLastKnownLocation() {
100        Location coarseLoc = null;
101        Location fineLoc = null;
102
103        int permission = PermissionChecker.checkSelfPermission(mContext,
104                Manifest.permission.ACCESS_COARSE_LOCATION);
105        if (permission == PermissionChecker.PERMISSION_GRANTED) {
106            coarseLoc = getLastKnownLocationForProvider(LocationManager.NETWORK_PROVIDER);
107        }
108
109        permission = PermissionChecker.checkSelfPermission(mContext,
110                Manifest.permission.ACCESS_FINE_LOCATION);
111        if (permission == PermissionChecker.PERMISSION_GRANTED) {
112            fineLoc = getLastKnownLocationForProvider(LocationManager.GPS_PROVIDER);
113        }
114
115        if (fineLoc != null && coarseLoc != null) {
116            // If we have both a fine and coarse location, use the latest
117            return fineLoc.getTime() > coarseLoc.getTime() ? fineLoc : coarseLoc;
118        } else {
119            // Else, return the non-null one (if there is one)
120            return fineLoc != null ? fineLoc : coarseLoc;
121        }
122    }
123
124    private Location getLastKnownLocationForProvider(String provider) {
125        if (mLocationManager != null) {
126            try {
127                if (mLocationManager.isProviderEnabled(provider)) {
128                    return mLocationManager.getLastKnownLocation(provider);
129                }
130            } catch (Exception e) {
131                Log.d(TAG, "Failed to get last known location", e);
132            }
133        }
134        return null;
135    }
136
137    private boolean isStateValid() {
138        return mTwilightState != null && mTwilightState.nextUpdate > System.currentTimeMillis();
139    }
140
141    private void updateState(@NonNull Location location) {
142        final TwilightState state = mTwilightState;
143        final long now = System.currentTimeMillis();
144        final TwilightCalculator calculator = TwilightCalculator.getInstance();
145
146        // calculate yesterday's twilight
147        calculator.calculateTwilight(now - DateUtils.DAY_IN_MILLIS,
148                location.getLatitude(), location.getLongitude());
149        final long yesterdaySunset = calculator.sunset;
150
151        // calculate today's twilight
152        calculator.calculateTwilight(now, location.getLatitude(), location.getLongitude());
153        final boolean isNight = (calculator.state == TwilightCalculator.NIGHT);
154        final long todaySunrise = calculator.sunrise;
155        final long todaySunset = calculator.sunset;
156
157        // calculate tomorrow's twilight
158        calculator.calculateTwilight(now + DateUtils.DAY_IN_MILLIS,
159                location.getLatitude(), location.getLongitude());
160        final long tomorrowSunrise = calculator.sunrise;
161
162        // Set next update
163        long nextUpdate = 0;
164        if (todaySunrise == -1 || todaySunset == -1) {
165            // In the case the day or night never ends the update is scheduled 12 hours later.
166            nextUpdate = now + 12 * DateUtils.HOUR_IN_MILLIS;
167        } else {
168            if (now > todaySunset) {
169                nextUpdate += tomorrowSunrise;
170            } else if (now > todaySunrise) {
171                nextUpdate += todaySunset;
172            } else {
173                nextUpdate += todaySunrise;
174            }
175            // add some extra time to be on the safe side.
176            nextUpdate += DateUtils.MINUTE_IN_MILLIS;
177        }
178
179        // Update the twilight state
180        state.isNight = isNight;
181        state.yesterdaySunset = yesterdaySunset;
182        state.todaySunrise = todaySunrise;
183        state.todaySunset = todaySunset;
184        state.tomorrowSunrise = tomorrowSunrise;
185        state.nextUpdate = nextUpdate;
186    }
187
188    /**
189     * Describes whether it is day or night.
190     */
191    private static class TwilightState {
192        boolean isNight;
193        long yesterdaySunset;
194        long todaySunrise;
195        long todaySunset;
196        long tomorrowSunrise;
197        long nextUpdate;
198
199        TwilightState() {
200        }
201    }
202}
203