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