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