Home | History | Annotate | Download | only in app
      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