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