Android附近基站+Wifi+IP+GPS多渠道定位方案

前言:

在移动客户端的开发中,地理位置定位是一个非常重要的环节,有些时候用户可能会限制web app或者Android app的一些权限,或者由于信号不佳的原因无法获得准确的GPS位置,甚至为了省电,用户可能对开启GPS开关可能会有抵触情绪。但是不能因为GPS的种种限制就放弃了对用户位置的追踪。要通过一切能发送出信号的物体尽可能准确的获取到用户的位置,有时可以牺牲一些精度,对于大数据和用户地区分布分析来说,有一个大体的位置已经够分析人员使用,而且绕开GPS的重重壁垒,为数据的完整性提供可靠方案

开发背景:

有些项目经理反应目前版本的app耗电量过大,状态栏的GPS标识一直在闪烁。于是为了降低软件的耗电量,同时保证定位的经纬度,并且不能仅依赖GPS进行定位,在用户不愿意开启GPS开关时,需要通过手机附近的基站,wifi热点,甚至IP地址进行定位。对提高用户体验有非常重要的作用。(之前的获取经纬度策略是使用Android提供的API,通过开启一个IntentService,在Service中开始一个死循环的子线程不停的进行GPS位置获取,相关代码可以看这里《使用Andorid原生工具类获取手机经纬度》

实现效果:

实现的效果可以参考高德地图,高德地图的使用体验我觉得是不错的,甚至完全不需要开启GPS开关,就可以准确定位到道路街道,误差一般不会超过50米,我猜测高德的实现方案无非也是wifi加基站。我做过一次测试,在一个收不到GPS信号的地下室中(但是附近有热点和基站),在非联网状态下打开高德地图,高德会把我定位到上次成功的位置,当我开启了网络开关,马上位置就八九不离十了。

最近整理成了demo,下面是截图

该demo的github地址,希望能和其他大神一起优化这个定位模块,变得更精确,更省电:https://github.com/AlexZhuo/AlxLocationManager

定位原理:

原理其实很简单,在GPS定位没有成功时(GPS定位一般是最准确的,具有最高的优先级),用手机扫描附近的基站,记录下附近基站的mcc,lac,cid等参数,用于识别该基站,通过数据库查询到该基站的GPS位置,用收到该基站的信号强度当做相对基站的距离,如果搜索到附近的基站比较多,就可以以基站的位置为圆心,以信号强度为半径进行画圆,多个基站画出圆圈重合的部分就是当前最可能的位置。同理wifi热点定位也是一样,不过用于识别热点的换成了MAC地址,一样用信号的强度作为距离的表示。

定位策略:

1、首先在开启app的时候,取系统中记录的最近一次GPS定位位置,如果取得的GPS精确度小于200m,那么就弃用,如果取到了正常的结果,就不需要位置监听和基站、wifi定位了,但是这在多数情况下是取不到的,尤其是刚刚打开GPS开关没多久的时候,这时候从SharedPreference里取出上次app定位成功的地点先顶上。

2、如果上一步没有马上取得当前的GPS位置,就会开启一个监听不断监测位置的变化,然而这个监听的要求是高灵敏度,高精度,高耗电的,此时状态栏的GPS标识会开始闪烁,直到取得第一次精确的GPS位置,如果取得的GPS精确度小于200m,那么就弃用。

3、在开启监听器的同时,开始扫描手机附近的基站信息和wifi热点信息,获取每个基站和热点的信号强度,通过数据库查询出每个基站和热点的GPS信息,然后通过相关数学算法求出手机的大致经纬度(在本文中这些是通过谷歌提供的API进行实现)但是如果取得的GPS精确度小于200m,那么就弃用。

4、监听器获取到当前GPS定位成功后,就关掉当前的监听器,然后开启一个策略不同的监听器,新监听器的主要特点是低精度,低灵敏度,低耗电,此时状态栏的GPS闪烁停止,但是定位的精度大幅降低,可能会跑到好几公里以外。

5、为了防止监听器后台监听耗电,被认为成后台偷跑电量的程序,需要在程序放到后台和关闭的时候关闭响应的监听。

6、渠道优先级:GPS>基站>WIFI热点>IP,这只是一个大体的安排,实际上在GPS信号不太好的地方,wifi加基站的定位结果可能要比GPS准确的多,所以具体采用哪种定位方式需要看他们的accuracy值

7、对于一些国产电信公司定制机,一般没有安装谷歌框架的,使用谷歌原生框架获取GPS位置,基站定位,wifi热点定位不变。

能正常定位的情景:

1、如果用户在开阔的室外(GPS开,wifi可关可开,sim卡可以不插,可以不联网),并且天气晴朗,一般可以收到良好的GPS信号,此时的定位结果应是GPS定位信号

2、如果用户在封闭的室内,且无法收到GPS信号,此时手机必须要联网才能获得定位信息,而且一下条件要二选一(1)手机要插sim卡且有信号(2)手机开启wifi开关且附近有热点。如果两种信号都收不到,那么会根据ip进行定位,且要在手机没有开VPN网络的时候

无法定位的情况:

1、用户在封闭的室内,且没有联网。但此时会采用SharePreference中的记录

2、用户在室外,有大雨,大雾,阴天,且没有联网。或者联网了但手机没有信号,附近没有wifi

运行环境:

1、首先需要引入Google Service gcm location SDK,引入这个SDK的原因是:省电。并且防止状态栏上的GPS标识一直闪烁,谷歌的官方文档说的很直白,不推荐使用Android原生的接口,原文如下

the Google
Play services location APIs
 are preferred over the Android framework location APIs (android.location)
as a way of adding location awareness to your app. If you are currently using the Android framework location APIs, you are strongly encouraged to switch to the Google Play services location APIs as soon as possible.

该SDK的官方文档地址:https://developer.android.com/training/location/index.html

引入该SDK需要在gradle文件里添加

compile 'com.google.android.gms:play-services-location:8.4.0'

注意:许多国产手机,尤其是运营商定制机里往往没有内置谷歌框架,此时的表现是一直无法connect GPS,这时候就不能用谷歌的SDK,而需要使用Android原生API,也就是《使用Andorid原生工具类获取手机经纬度》这篇文章中介绍的方法

2、开启相关权限

 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

注意:相关权限一旦用户没有授予或授予后驳回,可能会有crash的情况

主要的暴露方法:

void onCreateGPS(Application context)

用于开启一次gps位置获取,程序会先获取最近一次的GPS位置,如果失败的话就开启GPS追踪和基站,wifi定位等

 void restartGPS(Application context)

在进入一些对GPS要求比较高的页面时,重新获取当前的准确位置的方法,本方法会先暂停掉当前的GPS追踪,然后重新获取一遍位置

void stopGPS()

在退出app时,为了节省电力而彻底关闭GPS追踪

 void pauseGPS()

当程序放到后台时为了防止后台耗电而停止GPS跟踪

GPS的后台省电:

要监听app是否被放到了后台,Android系统没有给出相关接口或广播,只能通过Activity的Stop和Start的关系来判断,一般来说,一个Stop后如果紧跟着一个start那么就可以说这是内部的转换,但是如果只Stop没有start那么就很有可能是放到后台了。

第二种监听方法是,在Activity stop的时候观察一下本Activity是否还在栈顶,如果还在栈顶那么可以说明被放到后台了,下面这段代码就是这个思想,需要程序里用到的Activity基本都以这个BaseActivity为父类

import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;

import com.imaginato.qravedconsumer.service.AlxLocationManager;
import com.imaginato.qravedconsumer.utils.JLogUtils;

import java.util.List;

public class BaseActivity extends FragmentActivity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

	}

	@Override
	protected void onDestroy() {
		super.onDestroy();

	}

	@Override
	protected void onPause() {
		super.onPause();

	}

	@Override
	protected void onResume() {
		super.onResume();

	}

	@Override
	protected void onStart() {
		super.onStart();

	}

	@Override
	protected void onStop() {
		super.onStop();

		if (!isAppOnForeground(this)) {
			//app 进入后台
			JLogUtils.i("AlexLocation","程序进入后台运行");
			//全局变量isActive = false 记录当前已经进入后台
			isRunningBackGround = true;
			AlxLocationManager.pauseGPS();
		}
	}

	@Override
	protected void onRestart() {
		super.onRestart();
		//从后台恢复,如果还没有很好的获得经纬度就继续获得
		if(isRunningBackGround && AlxLocationManager.manager!=null && AlxLocationManager.manager.currentStatus!= AlxLocationManager.STATUS.TRYING_FIRST)AlxLocationManager.onCreateGPS(getApplication());
		isRunningBackGround = false;
	}

	/**
	 * 程序是否在前台运行
	 *
	 * @return
	 */
	public static boolean isAppOnForeground(Activity activity) {
		// Returns a list of application processes that are running on the
		// device

		ActivityManager activityManager = (ActivityManager) activity.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
		String packageName = activity.getApplicationContext().getPackageName();

		List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager
				.getRunningAppProcesses();
		if (appProcesses == null)
			return false;

		for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
			// The name of the process that this object is associated with.
			if (appProcess.processName.equals(packageName)
					&& appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
				return true;
			}
		}

		return false;
	}

}

谷歌API相关:

谷歌那里有全世界几乎所有基站的GPS位置,而且谷歌的数据库里还有全世界许多wifi热点的GPS位置,还有IP的归属地,所以对于我现在的支援东南亚国家的app来说使用谷歌的数据库再合适不过了。

谷歌API的主页:https://developers.google.com/maps/documentation/geolocation/intro

1、首先先申请一个密钥,申请非常简单,只要有一个谷歌账号,点一下“申请密钥”,几下就好了

2、使用postman等测试工具,使用谷歌给出的json示例看看好不好用

{ "homeMobileCountryCode": 310,//中国一般是460 "homeMobileNetworkCode": 260,//使用手机获取 "radioType": "gsm",//这个填起来要特别小心,宁愿不填,如果填错了的话有可能无法获得定位信息 "carrier": "T-Mobile",//运营商,作用不大 "cellTowers": [//基站列表  {   "cellId": 39627456,//必填项,比较重要   "locationAreaCode": 40495,   "mobileCountryCode": 310,//中国是460   "mobileNetworkCode": 260,   "age": 0,   "signalStrength": -95//相当于距离  } ], "wifiAccessPoints": [//wifi热点  {   "macAddress": "01:23:45:67:89:AB",   "signalStrength": 8,   "age": 0,   "signalToNoiseRatio": -65,   "channel": 8  },  {   "macAddress": "01:23:45:67:89:AC",   "signalStrength": 4,   "age": 0  } ]}

post接口地址:

https://www.googleapis.com/geolocation/v1/geolocate?key=YOUR_API_KEY

这里要注意一下,以前google也提供了一个类似的接口,但是从2012年开始就弃用了,很多老博客里还写着这个过时的接口,在江湖上流传已久,欺骗了不少无辜群众:http://www.google.com/loc/json

postman测试如图:(注意黄色的部分)

如果发送的数据格式有问题,谷歌会返回一些错误码,如下:

原因 HTTP 状态代码 说明
dailyLimitExceeded usageLimits 403 您已超出了您的每日限制
keyInvalid usageLimits 400 您的 API 密钥对于 Google Maps Geolocation API 无效。请确保您已加入了完整的密钥,而且您已购买该 API 或已启用收费和激活
API
 以获得免费配额。
userRateLimitExceeded usageLimits 403 您已超出您在 API 控制台中配置的每个用户每秒请求数限制。此限制应配置为防止单个或一小部分用户耗尽您每天的配额,同时还允许所有用户都能进行合理的访问。
notFound geolocation 404 请求有效,但未返回任何结果。
parseError global 400 请求正文不是有效的 JSON。请参阅请求正文部分以了解每个字段的详情。

开发中遇到的几个大坑:

1、谷歌的API接口只接收json的数据,这对安卓这边的发送造成了很多麻烦,一开始我用httpClient,经常碰到415异常,后来使用xUtils中的HttpUtil做json,经常收到404错误。后来知道,415异常应该是json放的位置不对,404异常不是post请求的问题,而是根据当前的基站,wifi信息没有查询到符合条件的地理位置,不是找不到服务器。

2、当我开开心心的把程序发给客户看以后,客户居然说定位太不准了,偏移了15公里,后来经过一番检查才发现,通过GPS获取到位置的accuracy太大,经常是2000,我一直以为GPS信号准确无比,后来才知道这也要对比精确度的。最关键的是我在第一次获取到精确的经纬度后开启了一个省电策略,而这个省电策略获得的经纬度精确度非常差,而我拿他当当前的经纬度来用,所以这种策略是不行的。省电模式要慎用!!

3、在测试位置过程中发现不管怎样定位都会偏移一块位置,我是在这个网站上根据经纬度查看地图坐标:http://www.gpsspg.com/maps.htm

后来我发现换一下输入的经纬度类型就好了,如下图黄色的部分

后来我发现,GPS获得的经纬度所用的坐标系和谷歌地图、百度地图上的坐标系是不同的,GPS硬件获得的经纬度是WGS-84标准,国际通用,可以看成是正常的GPS位置,在google earth上全部坐标,谷歌地图上除中国以外的坐标均为这个标准,但是谷歌地图的中国坐标使用的是GCJ-02标准,是天朝测绘局在WGS-84的基础上加入随机偏差加密后的一种经纬度,会随机偏移一公里左右,所以导致无论怎样定位总是显示不对。而百度地图在GCJ-02上又加入了自己的加密算法,变成了BD-09标准,另外其他很多的地图都有自己的加密方法。这些都是硬件获取经纬度在地图上指示不对的原因,网上有免费做转换的网站,在这里推荐一个:http://map.yanue.net/gps.html,如果发现GPS定位结果总是有误差,可以用这个转换一下。

关于GPS随意偏移,找到一篇比较好的文章,有兴趣的童鞋可以看一下:http://yanue.net/post-121.html

关于这几种坐标系的相关转换,网上除了有免费api以外还有一些公开算法,鉴于法律原因这里不再多讲,有需要的童鞋可以百度一下

4、最大的一个坑是:google gcm service也就是com.google.android.gms:play-services-location:8.4.0这个library是谷歌推荐的替换Android原生的一个GPS方案,但是这个SDK真的是太烂了太烂了太烂了!!!本来Android原生API可以支持network
passive gps定位,也就是没开GPS开关也可以根据附近的网络状况定位,准确度很高,但是这个库引进来之后只能根据GPS定位,而且很慢!!!很慢很慢,最坑的是:不准确!!!感觉这个SDK对它监测到的经纬度加上了偏移,不如Android原生的准确度高,总会偏移出去100m左右,真的让人很头疼,总之,这个库还是不用为好,如果需要获得精确的经纬度,还是推荐我以前的一篇博文《使用Andorid原生工具类获取手机经纬度》

主要代码:

下面是定位主要的实现代码,注释已经比较详尽,这里就不多赘述。

import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Location;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkInfo;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.telephony.CellIdentityCdma;
import android.telephony.CellIdentityGsm;
import android.telephony.CellIdentityLte;
import android.telephony.CellIdentityWcdma;
import android.telephony.CellInfo;
import android.telephony.CellInfoCdma;
import android.telephony.CellInfoGsm;
import android.telephony.CellInfoLte;
import android.telephony.CellInfoWcdma;
import android.telephony.CellLocation;
import android.telephony.NeighboringCellInfo;
import android.telephony.TelephonyManager;
import android.telephony.cdma.CdmaCellLocation;
import android.telephony.gsm.GsmCellLocation;
import android.util.Log;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;
import com.imaginato.location.IMGLocationUpdateService;
import com.imaginato.qravedconsumer.application.QravedApplication;
import com.imaginato.qravedconsumer.utils.JViewUtils;
import com.lidroid.xutils.HttpUtils;
import com.lidroid.xutils.exception.HttpException;
import com.lidroid.xutils.http.RequestParams;
import com.lidroid.xutils.http.ResponseInfo;
import com.lidroid.xutils.http.callback.RequestCallBack;
import com.lidroid.xutils.http.client.HttpRequest;
import com.qraved.app.R;

import org.apache.http.entity.StringEntity;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by AlexLocation on 2016/6/6.
 */
public class AlxLocationManager implements GoogleApiClient.ConnectionCallbacks,GoogleApiClient.OnConnectionFailedListener,LocationListener {
    //下面这三个是没拿到第一次经纬度的时候耗电抓取经纬度的策略

    private static final int MAX_deviation = 100;
    private static final int FAST_UPDATE_INTERVAL = 10000; // 10 sec 平均更新时间
    private static final int FATEST_INTERVAL = 5000; // 5 sec 最短更新时间
    private static final int FAST_DISPLACEMENT = 1; // 1 meters 为最小侦听距离

    //下面这个是省电抓取经纬度的策略
    private static final int SLOW_UPDATE_INTERVAL = 60000; // 60 sec 平均更新时间
    private static final int SLOW_INTERVAL = 30000; // 30 sec 最短更新时间
    private static final int SLOW_DISPLACEMENT = 500; // 500 meters 为最小侦听距离

    private Application context;//防止内存泄漏,不使用activity的引用
    private GoogleApiClient mGoogleApiClient;
    public static AlxLocationManager manager;//单例模式

    public STATUS currentStatus = STATUS.NOT_CONNECT;
    public float accuracy = 99999;//没有获得精度的时候是-1
    public double lastLatitude;
    public double lastLongitude;

    public enum STATUS{
        NOT_CONNECT,//没有连接相关硬件成功
        TRYING_FIRST,//第一次获取地理位置,此时gps指示图标闪烁,耗电量大
        LOW_POWER,//开启app拿到精确度的GPS之后,开启省电模式
        NOT_TRACK//当前没有开启跟踪模式
    }

    public final static boolean isDebugging = true;//调试模式开关

    /**
     * 注册gps监听服务
     * @param context
     */
    public static void onCreateGPS(final Application context){
        if(manager!=null && manager.mGoogleApiClient!=null)return;
        Log.i("AlexLocation","准备开启gps");
        manager = new AlxLocationManager();
        manager.context = context;
        manager.mGoogleApiClient  = new GoogleApiClient.Builder(context)
                .addConnectionCallbacks(manager)
                .addOnConnectionFailedListener(manager)
                .addApi(LocationServices.API)
                .build();
        manager.mGoogleApiClient.connect();
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                if(manager!=null && manager.currentStatus!=STATUS.NOT_CONNECT)return;//如果连接GPS硬件到现在还没成功
                Log.i("AlexLocation","该手机没有安装谷歌框架服务,准备启动旧版service");
                manager.context.startService(new Intent(manager.context, IMGLocationUpdateService.class));//使用安卓原生API获取地理位置
                new AsyncTask<Void,Void,String>(){//在子线程中获取附近的基站和wifi信息

                    @Override
                    protected String doInBackground(Void... params) {
                        GeoLocationAPI geoLocationAPI = null;
                        try {
                            geoLocationAPI = getCellInfo(manager.context);//得到基站信息,通过基站进行定位
                        }catch (Exception e){
                            Log.i("AlexLocation","获取附近基站信息出现异常",e);
                        }
                        if(geoLocationAPI ==null){
                            Log.i("AlexLocation","获取基站信息失败");
                            return "{}";
                        }
                        getWifiInfo(manager.context, geoLocationAPI);
                        String json = geoLocationAPI.toJson();//这里使用gson.toJson()会被混淆,推荐使用手动拼json
                        Log.i("AlexLocation","准备发给goggle的json是"+json);
                        return json;
                    }

                    @Override
                    protected void onPostExecute(String json) {
                        super.onPostExecute(json);
                        //开启子线程请求网络
                        if(manager!=null && context!=null)manager.sendJsonByPost(json,"https://www.googleapis.com/geolocation/v1/geolocate?key="+context.getString(R.string.google_location));
                    }
                }.execute();
            }
        },9000);
    }

    /**
     * 进入某些页面,重新刷GPS
     * @param context
     */
    public static void restartGPS(Application context){
        stopGPS();//先停止当前的GPS
        onCreateGPS(context);//重启GPS
    }

    /**
     * 停止gps服务,用来省电
     */
    public static void stopGPS(){
        if(manager==null)return;
        pauseGPS();
        manager.mGoogleApiClient = null;
        manager = null;
    }

    /**
     * 当app被放到后台时,暂停GPS
     */
    public static void pauseGPS(){
        Log.i("Alex","准备暂停GPS");
        if(manager==null || manager.mGoogleApiClient==null || manager.currentStatus==STATUS.NOT_CONNECT || manager.currentStatus == STATUS.NOT_TRACK)return;
        try {
            LocationServices.FusedLocationApi.removeLocationUpdates(manager.mGoogleApiClient, manager);
            manager.currentStatus = STATUS.NOT_CONNECT;
            if (manager.mGoogleApiClient.isConnected() || manager.mGoogleApiClient.isConnecting()) manager.mGoogleApiClient.disconnect();
            manager.mGoogleApiClient = null;
        }catch (Exception e) {
            Log.i("AlexLocation","暂停GPS出现异常",e);
        }
    }
    @Override
    public void onConnected(@Nullable Bundle bundle) {
        Log.i("AlexLocation","connect gps成功");
        if(currentStatus != STATUS.NOT_CONNECT)return;//有些手机会多次连接成功
        currentStatus = STATUS.TRYING_FIRST;
        if(!getCurrentLocation()) {//得到当前gps并记录
            //如果没有成功拿到当前经纬度,那么就通过实时位置监听去不断的拿,直到拿到为止
            //如果没有拿到经纬度,就一直监听
            LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, createFastLocationRequest(), this);//创建位置监听
            new AsyncTask<Void,Void,String>(){//在子线程中获取附近的基站和wifi信息

                @Override
                protected String doInBackground(Void... params) {
                    GeoLocationAPI geoLocationAPI = null;
                    try {
                        geoLocationAPI = getCellInfo(context);//得到基站信息,通过基站进行定位
                    }catch (Exception e){
                        Log.i("AlexLocation","获取附近基站信息出现异常",e);
                    }
                    if(geoLocationAPI ==null){
                        Log.i("AlexLocation","获取基站信息失败");
                        return "{}";
                    }
                    getWifiInfo(context, geoLocationAPI);
                    String json = geoLocationAPI.toJson();//这里使用gson.toJson()会被混淆,推荐使用手动拼json
                    Log.i("AlexLocation","准备发给goggle的json是"+json);
                    return json;
                }

                @Override
                protected void onPostExecute(String json) {
                    super.onPostExecute(json);
                    //发送json数据到谷歌,等待谷歌返回结果
                    sendJsonByPost(json,"https://www.googleapis.com/geolocation/v1/geolocate?key="+context.getString(R.string.google_location));
                }
            }.execute();

        }else {//获取了最后一次硬件记录的经纬度,不进行追踪
            currentStatus = STATUS.NOT_TRACK;
        }
    }

    /**
     * 拿到最近一次的硬件经纬度记录,只用精确度足够高的时候才会采用这种定位
     * @return
     */
    public boolean getCurrentLocation(){
        Location mLastLocation = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient);
        Log.i("AlexLocation","得到last Location的gps是=="+mLastLocation);
        if (mLastLocation == null) return false;//在少数情况下这里有可能是null
        double latitude = mLastLocation.getLatitude();//纬度
        double longitude = mLastLocation.getLongitude();//经度
        double altitude = mLastLocation.getAltitude();//海拔
        float last_accuracy = mLastLocation.getAccuracy();//精度
        Log.i("AlexLocation","last Location的精度是"+last_accuracy);
        String provider = mLastLocation.getProvider();//传感器
        float bearing = mLastLocation.getBearing();
        float speed = mLastLocation.getSpeed();//速度
        if(isDebugging)JViewUtils.showToast(context,"获取到last location经纬度","纬度"+latitude+"  经度"+longitude+ "精确度"+last_accuracy);
        Log.i("AlexLocation","获取last location成功,纬度=="+latitude+"  经度"+longitude+"  海拔"+altitude+"   传感器"+provider+"   速度"+speed+ "精确度"+last_accuracy);
        if(last_accuracy<MAX_deviation){
            recordLocation(context,latitude,longitude,accuracy);
            this.accuracy = last_accuracy;
        }else {
            Log.i("AlexLocation","精确度太低,放弃last Location");
        }
        return last_accuracy<60;
    }

    @Override
    public void onConnectionSuspended(int i) {

    }

    /**
     * 注册完位置跟踪策略后,每隔一段时间会调用的这个方法,同时会拿到当前的位置
     * @param location
     */
    @Override
    public void onLocationChanged(Location location) {
        if(currentStatus == STATUS.LOW_POWER)Log.i("Alex","现在是低电力的定位策略");
        Log.i("AlexLocation","位置改变了"+location);
        if(location==null)return;
        if(isDebugging)JViewUtils.showToast(context,"获取到了最新的GPS",location.toString()+" 精度是"+location.getAccuracy());
        if(location.getAccuracy()<MAX_deviation && (location.getAccuracy()<=accuracy || getGPSDistance(location.getLatitude(),location.getLongitude(),lastLatitude,lastLongitude)>300)){//精度如果太小就放弃,如果精度提高或者移动的距离超过300m也更新
            recordLocation(context,location.getLatitude(),location.getLongitude(),location.getAccuracy());
            this.accuracy = location.getAccuracy();
        }else {
            Log.i("Alex","精确度太低,准备放弃最新的位置");
        }
        if(location.getAccuracy()>50 || currentStatus!=STATUS.TRYING_FIRST)return;//如果现在是高电量模式,那么就停止当前监听,采用低电量监听
        LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient,this);//成功获取到之后就降低频率来省电
        Log.i("AlexLocation","准备开启省电策略");
        currentStatus = STATUS.LOW_POWER;
        LocationServices.FusedLocationApi.requestLocationUpdates(mGoogleApiClient, createLowPowerLocationRequest(), this);//创建位置监听
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {

    }

    /**
     * 当手机移动后通过回调获得移动后的经纬度,在这个函数里配置相应的刷新频率
     * 建立一个耗电的,尽快的拿到当前经纬度的策略
     */
    private static LocationRequest createFastLocationRequest() {
        LocationRequest mLocationRequest = LocationRequest.create();
        mLocationRequest.setInterval(FAST_UPDATE_INTERVAL);
        mLocationRequest.setFastestInterval(FATEST_INTERVAL);
        mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);//耗电模式
        mLocationRequest.setSmallestDisplacement(FAST_DISPLACEMENT);
        return mLocationRequest;
    }

    /**
     * 建立一个省电的跟踪经纬度的策略
     * 注意此方法要慎用,使用了低电量模式以后,精确度会大幅下降
     * @return
     */
    private static LocationRequest createLowPowerLocationRequest() {
        LocationRequest mLocationRequest = LocationRequest.create();
        mLocationRequest.setInterval(SLOW_UPDATE_INTERVAL);
        mLocationRequest.setFastestInterval(SLOW_INTERVAL);
        mLocationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);//省电模式,PRIORITY_LOW_POWER要慎用,会导致定位精确度大幅下降,能偏到好几十公里以外去
        mLocationRequest.setSmallestDisplacement(SLOW_DISPLACEMENT);
        return mLocationRequest;
    }

    /**
     * 将获取到的经纬度记录在本地
     * @param context
     */
    public static void recordLocation(Context context, double latitude, double longitude,float accuracy){
        SharedPreferences sharedPreferences = context.getSharedPreferences("QravedApplicationSetting", Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putString("latitude", String.valueOf(latitude));
        editor.putString("longitude", String.valueOf(longitude));
        editor.putFloat("accuracy",accuracy);
        editor.apply();
        QravedApplication.getPhoneConfiguration().getLocation().latitude = latitude;
        QravedApplication.getPhoneConfiguration().getLocation().longitude = longitude;
        if(manager==null)return;
        manager.lastLatitude = latitude;
        manager.lastLongitude = longitude;
    }

    /**
     * 从sharedPreference中获取上次开启app时候的地理位置,前面的是纬度,后面的是经度
     * @return
     */
    public static double[] getOldLocation(Context context){
        SharedPreferences sharedPreferences = context.getSharedPreferences("QravedApplicationSetting", Context.MODE_PRIVATE);
        String latitudeStr = sharedPreferences.getString("latitude","");
        String longitudeStr = sharedPreferences.getString("longitude","");
        float accuracy = sharedPreferences.getFloat("accuracy",9999);
        Log.i("AlexLocation","SP里的精确度"+accuracy);
        if(latitudeStr.length()==0 || longitudeStr.length()==0)return null;
        double[] latlng = {-1,-1};
        try {
            latlng[0] = new Double(latitudeStr);
            latlng[1] = new Double(longitudeStr);
        }catch (Exception e){
            Log.i("AlexLocation","解析经纬度出现异常",e);
        }
        return latlng;
    }

    /**
     * 得到附近的基站信息,准备传给谷歌
     */
    public static GeoLocationAPI getCellInfo(Context context){
        //通过TelephonyManager 获取lac:mcc:mnc:cell-id
        GeoLocationAPI cellInfo = new GeoLocationAPI();
        TelephonyManager mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
        if(mTelephonyManager==null)return cellInfo;
        // 返回值MCC + MNC
        /*# MCC,Mobile Country Code,移动国家代码(中国的为460);
        * # MNC,Mobile Network Code,移动网络号码(中国移动为0,中国联通为1,中国电信为2);
        * # LAC,Location Area Code,位置区域码;
        * # CID,Cell Identity,基站编号;
        * # BSSS,Base station signal strength,基站信号强度。
        */
        String operator = mTelephonyManager.getNetworkOperator();
        Log.i("AlexLocation","获取的基站信息是"+operator);
        if(operator==null || operator.length()<5){
            Log.i("AlexLocation","获取基站信息有问题,可能是手机没插sim卡");
            return cellInfo;
        }
        int mcc = Integer.parseInt(operator.substring(0, 3));
        int mnc = Integer.parseInt(operator.substring(3));
        int lac;
        int cellId;
        // 中国移动和中国联通获取LAC、CID的方式
        CellLocation cellLocation = mTelephonyManager.getCellLocation();
        if(cellLocation==null){
            Log.i("AlexLocation","手机没插sim卡吧");
            return cellInfo;
        }
        if(mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) {
            Log.i("AlexLocation","当前是gsm基站");
            GsmCellLocation location = (GsmCellLocation)cellLocation;
            lac = location.getLac();
            cellId = location.getCid();
            //这些东西非常重要,是根据基站获得定位的重要依据
            Log.i("AlexLocation", " MCC移动国家代码 = " + mcc + "\t MNC移动网络号码 = " + mnc + "\t LAC位置区域码 = " + lac + "\t CID基站编号 = " + cellId);
        }else if(mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) {
            // 中国电信获取LAC、CID的方式
            Log.i("AlexLocation","现在是cdma基站");
            CdmaCellLocation location1 = (CdmaCellLocation) mTelephonyManager.getCellLocation();
            lac = location1.getNetworkId();
            cellId = location1.getBaseStationId();
            cellId /= 16;
        }else {
            Log.i("AlexLocation","现在不知道是什么基站");
            return cellInfo;
        }
        cellInfo.radioType = determine2g3g4g(context);//这里填要慎重,要是填的不对就会报404 notFound
        cellInfo.homeMobileCountryCode = mcc;
        cellInfo.homeMobileNetworkCode = mnc;
        cellInfo.carrier = getCarrier(operator);
        cellInfo.considerIp = considerIP(context);//这里要判断是否采用了vpn,在wifi的时候可以用ip辅助定位,但是如果是用的2g3g4g信号那就只能用基站,ip会不准
        ArrayList<GoogleCellTower>towers = new ArrayList<>(1);
        GoogleCellTower bigTower = new GoogleCellTower();//这个塔是当前连接的塔,只有一个,但是对定位有决定性的作用
        bigTower.cellId = cellId;
        bigTower.mobileCountryCode = mcc;
        bigTower.mobileNetworkCode = mnc;
        bigTower.locationAreaCode = lac;
        bigTower.signalStrength = 0;
        towers.add(bigTower);
        cellInfo.cellTowers = towers;
        // 获取邻区基站信息
        if(Build.VERSION.SDK_INT<17) {//低版的android系统使用getNeighboringCellInfo方法
            List<NeighboringCellInfo> infos = mTelephonyManager.getNeighboringCellInfo();
            if(infos==null){
                Log.i("AlexLocation","手机型号不支持基站定位1");
                return cellInfo;
            }
            if(infos.size()==0)return cellInfo;//附近没有基站
            towers = new ArrayList<>(infos.size());
            StringBuffer sb = new StringBuffer("附近基站总数 : " + infos.size() + "\n");
            for (NeighboringCellInfo info1 : infos) { // 根据邻区总数进行循环
                GoogleCellTower tower = new GoogleCellTower();
                sb.append(" LAC : " + info1.getLac()); // 取出当前邻区的LAC
                tower.locationAreaCode = info1.getLac();
                tower.mobileCountryCode = mcc;
                tower.mobileNetworkCode = mnc;
                tower.signalStrength = info1.getRssi();
                sb.append(" CID : " + info1.getCid()); // 取出当前邻区的CID
                tower.cellId = info1.getCid();
                sb.append(" BSSS : " + (-113 + 2 * info1.getRssi()) + "\n"); // 获取邻区基站信号强度
                towers.add(tower);
            }
            Log.i("AlexLocation","基站信息是"+sb);
        }else {//高版android系统使用getAllCellInfo方法,并且对基站的类型加以区分
            List<CellInfo> infos = mTelephonyManager.getAllCellInfo();
            if(infos!=null) {
                if(infos.size()==0)return cellInfo;
                towers = new ArrayList<>(infos.size());
                for (CellInfo i : infos) { // 根据邻区总数进行循环
                    Log.i("AlexLocation", "附近基站信息是" + i.toString());
                    GoogleCellTower tower = new GoogleCellTower();
                    if(i instanceof CellInfoGsm){//这里的塔分为好几种类型
                        Log.i("AlexLocation","现在是gsm基站");
                        CellIdentityGsm cellIdentityGsm = ((CellInfoGsm)i).getCellIdentity();//从这个类里面可以取出好多有用的东西
                        if(cellIdentityGsm==null)continue;
                        tower.locationAreaCode = cellIdentityGsm.getLac();
                        tower.mobileCountryCode = cellIdentityGsm.getMcc();
                        tower.mobileNetworkCode = cellIdentityGsm.getMnc();
                        tower.signalStrength = 0;
                        tower.cellId = cellIdentityGsm.getCid();
                    }else if(i instanceof CellInfoCdma){
                        Log.i("AlexLocation","现在是cdma基站");
                        CellIdentityCdma cellIdentityCdma = ((CellInfoCdma)i).getCellIdentity();
                        if(cellIdentityCdma==null)continue;
                        tower.locationAreaCode = lac;
                        tower.mobileCountryCode = mcc;
                        tower.mobileNetworkCode = cellIdentityCdma.getSystemId();//cdma用sid,是系统识别码,每个地级市只有一个sid,是唯一的。
                        tower.signalStrength = 0;
                        cellIdentityCdma.getNetworkId();//NID是网络识别码,由各本地网管理,也就是由地级分公司分配。每个地级市可能有1到3个nid。
                        tower.cellId = cellIdentityCdma.getBasestationId();//cdma用bid,表示的是网络中的某一个小区,可以理解为基站。
                    }else if(i instanceof CellInfoLte) {
                        Log.i("AlexLocation", "现在是lte基站");
                        CellIdentityLte cellIdentityLte = ((CellInfoLte) i).getCellIdentity();
                        if(cellIdentityLte==null)continue;
                        tower.locationAreaCode = lac;
                        tower.mobileCountryCode = cellIdentityLte.getMcc();
                        tower.mobileNetworkCode = cellIdentityLte.getMnc();
                        tower.cellId = cellIdentityLte.getCi();
                        tower.signalStrength = 0;
                    }else if(i instanceof CellInfoWcdma && Build.VERSION.SDK_INT>=18){
                        Log.i("AlexLocation","现在是wcdma基站");
                        CellIdentityWcdma cellIdentityWcdma = ((CellInfoWcdma)i).getCellIdentity();
                        if(cellIdentityWcdma==null)continue;
                        tower.locationAreaCode = cellIdentityWcdma.getLac();
                        tower.mobileCountryCode = cellIdentityWcdma.getMcc();
                        tower.mobileNetworkCode = cellIdentityWcdma.getMnc();
                        tower.cellId = cellIdentityWcdma.getCid();
                        tower.signalStrength = 0;
                    }else {
                        Log.i("AlexLocation","不知道现在是啥基站");
                    }
                    towers.add(tower);
                }
            }else {//有些手机拿不到的话,就用废弃的方法,有时候即使手机支持,getNeighboringCellInfo的返回结果也常常是null
                Log.i("AlexLocation","通过高版本SDK无法拿到基站信息,准备用低版本的方法");
                List<NeighboringCellInfo> infos2 = mTelephonyManager.getNeighboringCellInfo();
                if(infos2==null || infos2.size()==0){
                    Log.i("AlexLocation","该手机确实不支持基站定位,已经无能为力了");
                    return cellInfo;
                }
                towers = new ArrayList<>(infos2.size());
                StringBuffer sb = new StringBuffer("附近基站总数 : " + infos2.size() + "\n");
                for (NeighboringCellInfo i : infos2) { // 根据邻区总数进行循环
                    GoogleCellTower tower = new GoogleCellTower();
                    sb.append(" LAC : " + i.getLac()); // 取出当前邻区的LAC
                    tower.age = 0;
                    tower.locationAreaCode = i.getLac();
                    tower.mobileCountryCode = mcc;
                    tower.mobileNetworkCode = mnc;
                    sb.append(" CID : " + i.getCid()); // 取出当前邻区的CID
                    tower.cellId = i.getCid();
                    sb.append(" BSSS : " + (-113 + 2 * i.getRssi()) + "\n"); // 获取邻区基站信号强度
                    towers.add(tower);
                }
                Log.i("AlexLocation","基站信息是"+sb);
            }
        }
        cellInfo.cellTowers = towers;
        return cellInfo;
    }

    /**
     * 看看现在用wifi流量还是手机流量,如果是wifi返回true
     * @param context
     * @return
     */
    public static boolean isWifiEnvironment(Context context){
        ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
        if(networkInfo==null){
            Log.i("AlexLocation","现在没有联网");
            return false;
        }
        int netType = networkInfo.getType();
        switch (netType){
            case ConnectivityManager.TYPE_WIFI:
                Log.i("AlexLocation","现在是wifi网络,可以用ip定位");
                return true;
            case ConnectivityManager.TYPE_VPN://这个基本没用
                Log.i("AlexLocation","现在是VPN网络");
                break;
            case ConnectivityManager.TYPE_MOBILE:
                Log.i("AlexLocation","现在是移动网络,不能用ip定位");
                int subType = networkInfo.getSubtype();
                Log.i("AlexLocation","移动网络子类是"+subType+"  "+networkInfo.getSubtypeName());//能判断是2g/3g/4g网络
                break;
            default:
                Log.i("AlexLocation","不知道现在是什么网络");
                break;
        }
        return false;
    }

    /**
     * 看看现在是wifi联网还是用的流量,如果是wifi返回true,因为wifi的时候可以用ip定位,但如果这时候是vpn,那就不能用ip定位
     * @param context
     */
    public static boolean considerIP(Context context){
        boolean considerIP = true;//默认是考虑
        ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        if(connectivityManager==null)return true;
        if(!isWifiEnvironment(context))return false;//如果现在不是wifi网络,就不能用ip定位
        if(Build.VERSION.SDK_INT<21) {//旧版本安卓获取网络状态
            NetworkInfo[] networkInfos = connectivityManager.getAllNetworkInfo();
            if(networkInfos==null)return true;
            for(NetworkInfo i:networkInfos){
                if(i==null)continue;
                Log.i("AlexLocation","现在的网络是"+i.getTypeName()+i.getType()+"   "+i.getSubtypeName());//WIFI,VPN,MOBILE+LTE
                if(i.getType()==ConnectivityManager.TYPE_VPN){
                    Log.i("AlexLocation","现在用的是VPN网络,不能用ip定位");
                    considerIP = false;
                    break;
                }
            }
        }else {//新版
            Network[] networks = connectivityManager.getAllNetworks();
            if(networks==null)return true;
            for(Network n:networks){
                if(n==null)continue;
                NetworkInfo networkInfo = connectivityManager.getNetworkInfo(n);
                if(networkInfo==null)continue;
                Log.i("AlexLocation","现在的网络是"+networkInfo.getTypeName()+networkInfo.getType()+"   "+networkInfo.getSubtypeName());//WIFI,VPN,MOBILE+LTE
                if(networkInfo.getType()==ConnectivityManager.TYPE_VPN){
                    Log.i("AlexLocation","现在用的是VPN网络,不能用ip定位");
                    considerIP = false;
                    break;
                }
            }
        }
        return considerIP;
    }

    /**
     * 判断当前手机在2g,3g,还是4g,用于发给谷歌
     */
    public static String determine2g3g4g(Context context){
        ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        if(connectivityManager==null)return null;
        if(Build.VERSION.SDK_INT<21) {//旧版本安卓获取网络状态
            NetworkInfo[] networkInfos = connectivityManager.getAllNetworkInfo();
            if(networkInfos==null)return null;
            for(NetworkInfo i:networkInfos){
                if(i==null)continue;
                Log.i("AlexLocation","正在查看当前网络的制式"+i.getTypeName()+i.getType()+"   "+i.getSubtypeName());//WIFI,VPN,MOBILE+LTE
                if(i.getType()!=ConnectivityManager.TYPE_MOBILE)continue;//只看流量
                else Log.i("AlexLocation","现在是移动网络");
                return determine2g3g4g(i);
            }
        }else {//新版
            Network[] networks = connectivityManager.getAllNetworks();
            if(networks==null)return null;
            for(Network n:networks){
                if(n==null)continue;
                NetworkInfo networkInfo = connectivityManager.getNetworkInfo(n);
                if(networkInfo==null)continue;
                Log.i("AlexLocation","正在查看当前网络的制式"+networkInfo.getTypeName()+networkInfo.getType()+"   "+networkInfo.getSubtypeName());//WIFI,VPN,MOBILE+LTE
                if(networkInfo.getType()!=ConnectivityManager.TYPE_MOBILE) continue;//只看流量
                return determine2g3g4g(networkInfo);
            }
        }
        return null;
    }

    /**
     * 看看现在用的是几g,什么网络制式
     * @param info
     * @return
     */
    public static String determine2g3g4g(NetworkInfo info){
        if(info==null)return null;
        switch (info.getSubtype()){
            case TelephonyManager.NETWORK_TYPE_LTE:
                Log.i("AlexLocation","手机制式是lte");
                return "lte";
            case TelephonyManager.NETWORK_TYPE_EDGE:
                Log.i("AlexLocation","手机制式是edge");
                break;
            case TelephonyManager.NETWORK_TYPE_CDMA:
                return "cdma";
            case TelephonyManager.NETWORK_TYPE_GPRS:
                break;
            case TelephonyManager.NETWORK_TYPE_HSDPA:
                break;
            case TelephonyManager.NETWORK_TYPE_HSPA:
                break;
            case TelephonyManager.NETWORK_TYPE_HSPAP:
                break;
            case TelephonyManager.NETWORK_TYPE_HSUPA:
                break;
            case TelephonyManager.NETWORK_TYPE_EVDO_0:
                break;
            case TelephonyManager.NETWORK_TYPE_EVDO_A:
                break;
            case TelephonyManager.NETWORK_TYPE_EVDO_B:
                break;
            case TelephonyManager.NETWORK_TYPE_IDEN:
                break;
            case TelephonyManager.NETWORK_TYPE_UMTS:
                break;
            case TelephonyManager.NETWORK_TYPE_EHRPD:
                break;
            case TelephonyManager.NETWORK_TYPE_1xRTT:
                break;
            case TelephonyManager.NETWORK_TYPE_UNKNOWN:
                break;
        }
        return null;

    }

    /**
     * 得到附近的wifi信息,准备传给谷歌
     * @param context
     * @param geoLocationAPI
     * @return
     */
    public static GeoLocationAPI getWifiInfo(Context context, GeoLocationAPI geoLocationAPI){
        WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
        if(wifiManager == null)return geoLocationAPI;
        Log.i("AlexLocation","准备开始扫描附近wifi");
        wifiManager.startScan();
        //准备所有附近wifi放到wifi列表里,包括现在正连着的wifi
        ArrayList<AlxScanWifi> lsAllWIFI = new ArrayList<AlxScanWifi>();
        List<ScanResult> lsScanResult = wifiManager.getScanResults();//记录所有附近wifi的搜索结果
        if(lsScanResult == null){
            Log.i("AlexLocation","搜索附近wifi热点失败");
            return geoLocationAPI;
        }
        for (ScanResult result : lsScanResult) {
            Log.i("AlexLocation","发现一个附近的wifi::"+result.SSID+"  mac地址是"+result.BSSID+"   信号强度是"+result.level);
            if(result == null)continue;
            AlxScanWifi scanWIFI = new AlxScanWifi(result);
            lsAllWIFI.add(scanWIFI);//防止重复
        }
        ArrayList<GoogleWifiInfo> wifiInfos = new ArrayList<>(lsAllWIFI.size());
        for (AlxScanWifi w:lsAllWIFI){
            if(w == null)continue;
            GoogleWifiInfo wifiInfo = new GoogleWifiInfo();
            wifiInfo.macAddress = w.mac.toUpperCase();//记录附近每个wifi路由器的mac地址
            wifiInfo.signalStrength = w.dBm;//通过信号强度来判断距离
            wifiInfo.channel = w.channel;//通过信道来判断ssid是否为同一个
            wifiInfos.add(wifiInfo);
        }
        geoLocationAPI.wifiAccessPoints = wifiInfos;
        return geoLocationAPI;
    }

    /**
     * 扫描附近wifi之后,记录wifi节点信息的类
     */
    public static class AlxScanWifi implements Comparable<AlxScanWifi> {
        public final int dBm;
        public final String ssid;
        public final String mac;
        public short channel;
        public AlxScanWifi(ScanResult scanresult) {
            dBm = scanresult.level;
            ssid = scanresult.SSID;
            mac = scanresult.BSSID;//BSSID就是传说中的mac
            channel = getChannelByFrequency(scanresult.frequency);
        }
        public AlxScanWifi(String s, int i, String s1,String imac) {
            dBm = i;
            ssid = s1;
            mac = imac;
        }

        /**
         * 根据信号强度进行排序
         * @param wifiinfo
         * @return
         */
        public int compareTo(AlxScanWifi wifiinfo) {
            int i = wifiinfo.dBm;
            int j = dBm;
            return i - j;
        }

        /**
         * 为了防止添加wifi的列表重复,复写equals方法
         * @param obj
         * @return
         */
        public boolean equals(Object obj) {
            boolean flag = false;
            if (obj == this) {
                flag = true;
                return flag;
            } else {
                if (obj instanceof AlxScanWifi) {
                    AlxScanWifi wifiinfo = (AlxScanWifi) obj;
                    int i = wifiinfo.dBm;
                    int j = dBm;
                    if (i == j) {
                        String s = wifiinfo.mac;
                        String s1 = this.mac;
                        if (s.equals(s1)) {
                            flag = true;
                            return flag;
                        }
                    }
                    flag = false;
                } else {
                    flag = false;
                }
            }
            return flag;
        }
        public int hashCode() {
            int i = dBm;
            int j = mac.hashCode();
            return i ^ j;
        }

    }

    /**
     * 根据频率获得信道
     *
     * @param frequency
     * @return
     */
    public static short getChannelByFrequency(int frequency) {
        short channel = -1;
        switch (frequency) {
            case 2412:
                channel = 1;
                break;
            case 2417:
                channel = 2;
                break;
            case 2422:
                channel = 3;
                break;
            case 2427:
                channel = 4;
                break;
            case 2432:
                channel = 5;
                break;
            case 2437:
                channel = 6;
                break;
            case 2442:
                channel = 7;
                break;
            case 2447:
                channel = 8;
                break;
            case 2452:
                channel = 9;
                break;
            case 2457:
                channel = 10;
                break;
            case 2462:
                channel = 11;
                break;
            case 2467:
                channel = 12;
                break;
            case 2472:
                channel = 13;
                break;
            case 2484:
                channel = 14;
                break;
            case 5745:
                channel = 149;
                break;
            case 5765:
                channel = 153;
                break;
            case 5785:
                channel = 157;
                break;
            case 5805:
                channel = 161;
                break;
            case 5825:
                channel = 165;
                break;
        }
        Log.i("AlexLocation","信道是"+channel);
        return channel;
    }

    /**
     * 根据国家代码获取通信运营商名字
     * @param operatorString
     * @return
     */
    public static String getCarrier(String operatorString){
        if(operatorString == null)
        {
            return "0";
        }

        if(operatorString.equals("46000") || operatorString.equals("46002"))
        {
            //中国移动
            return "中国移动";
        }
        else if(operatorString.equals("46001"))
        {
            //中国联通
            return "中国联通";
        }
        else if(operatorString.equals("46003"))
        {
            //中国电信
            return "中国电信";
        }

        //error
        return "未知";
    }

    /**
     * 用于向谷歌根据基站请求经纬度的封装基站信息的类
     */
    public static class GeoLocationAPI {

        /**
         * homeMobileCountryCode : 310 移动国家代码(中国的为460);
         * homeMobileNetworkCode : 410 和基站有关
         * radioType : gsm
         * carrier : Vodafone 运营商名称
         * considerIp : true
         * cellTowers : []
         * wifiAccessPoints : []
         */

        public int homeMobileCountryCode;//设备的家庭网络的移动国家代码 (MCC)
        public int homeMobileNetworkCode;//设备的家庭网络的移动网络代码 (MNC)。
        public String radioType;//radioType:移动无线网络类型。支持的值有 lte、gsm、cdma 和 wcdma。虽然此字段是可选的,但如果提供了相应的值,就应该将此字段包括在内,以获得更精确的结果。
        public String carrier;//运营商名称。
        public boolean considerIp;//指定当 Wi-Fi 和移动电话基站的信号不可用时,是否回退到 IP 地理位置。请注意,请求头中的 IP 地址不能是设备的 IP 地址。默认为 true。将 considerIp 设置为 false 以禁用回退。
        public List<GoogleCellTower> cellTowers;
        public List<GoogleWifiInfo> wifiAccessPoints;

        public String toJson(){
            JSONObject jsonObject = new JSONObject();
            try {
                jsonObject.put("homeMobileCountryCode",homeMobileCountryCode);
                jsonObject.put("homeMobileNetworkCode",homeMobileNetworkCode);
                jsonObject.put("radioType",radioType);
                jsonObject.put("carrier",carrier);
                jsonObject.put("considerIp",considerIp);
                if(cellTowers!=null){
                    JSONArray jsonArray = new JSONArray();
                    for (GoogleCellTower t:cellTowers) jsonArray.put(t.toJson());
                    jsonObject.put("cellTowers",jsonArray);
                }
                if(wifiAccessPoints!=null){
                    JSONArray jsonArray = new JSONArray();
                    for (GoogleWifiInfo w:wifiAccessPoints) jsonArray.put(w.toJson());
                    jsonObject.put("wifiAccessPoints",jsonArray);
                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
            return jsonObject.toString();

        }

    }

    /**
     * 封装和基站有关的数据,准备发给谷歌
     */
    public static class GoogleCellTower {
        /*
        GSM:
                {
          "cellTowers": [
            {
              "cellId": 42,
              "locationAreaCode": 415,
              "mobileCountryCode": 310,
              "mobileNetworkCode": 410,
              "age": 0,
              "signalStrength": -60,
              "timingAdvance": 15
            }
          ]
        }
        WCDMA
        {
          "cellTowers": [
            {
              "cellId": 21532831,
              "locationAreaCode": 2862,
              "mobileCountryCode": 214,
              "mobileNetworkCode": 7
            }
          ]
        }

         */
        //下面的是必填
        int cellId;//(必填):小区的唯一标识符。在 GSM 上,这就是小区 ID (CID);CDMA 网络使用的是基站 ID (BID)。WCDMA 网络使用 UTRAN/GERAN 小区标识 (UC-Id),这是一个 32 位的值,由无线网络控制器 (RNC) 和小区 ID 连接而成。在 WCDMA 网络中,如果只指定 16 位的小区 ID 值,返回的结果可能会不准确。
        int locationAreaCode;//(必填):GSM 和 WCDMA 网络的位置区域代码 (LAC)。CDMA 网络的网络 ID (NID)。
        int mobileCountryCode;//(必填):移动电话基站的移动国家代码 (MCC)。
        int mobileNetworkCode;//(必填):移动电话基站的移动网络代码。对于 GSM 和 WCDMA,这就是 MNC;CDMA 使用的是系统 ID (SID)。
        int signalStrength;//测量到的无线信号强度(以 dBm 为单位)。
        //下面的是选填
        int age;//自从此小区成为主小区后经过的毫秒数。如果 age 为 0,cellId 就表示当前的测量值。
        int timingAdvance;//时间提前值。

        public JSONObject toJson(){
            JSONObject jsonObject = new JSONObject();
            try {
                jsonObject.put("cellId",cellId);
                jsonObject.put("locationAreaCode",locationAreaCode);
                jsonObject.put("mobileCountryCode",mobileCountryCode);
                jsonObject.put("mobileNetworkCode",mobileNetworkCode);
                jsonObject.put("signalStrength",signalStrength);
                jsonObject.put("age",age);
                jsonObject.put("timingAdvance",timingAdvance);
            } catch (JSONException e) {
                e.printStackTrace();
            }
            return jsonObject;
        }
    }

    /**
     * 向谷歌服务器根据附近wifi请求位置的json
     */
    public static class GoogleWifiInfo {

        /**
         * macAddress : 01:23:45:67:89:AB
         * signalStrength : -65
         * age : 0
         * channel : 11
         * signalToNoiseRatio : 40
         */

        public String macAddress;//(必填)Wi-Fi 节点的 MAC 地址。分隔符必须是 :(冒号),并且十六进制数字必须使用大写字母。
        public int signalStrength;//测量到的当前信号强度(以 dBm 为单位)。
        public int age;//自从检测到此接入点后经过的毫秒数。
        public short channel;//客户端与接入点进行通信的信道
        public int signalToNoiseRatio;//测量到的当前信噪比(以 dB 为单位)。

        public JSONObject toJson(){
            JSONObject jsonObject = new JSONObject();
            try {
                jsonObject.put("signalStrength",signalStrength);
                jsonObject.put("age",age);
                jsonObject.put("macAddress",macAddress);
                jsonObject.put("channel",channel);
                jsonObject.put("signalToNoiseRatio",signalToNoiseRatio);
            } catch (JSONException e) {
                e.printStackTrace();
            }
            return  jsonObject;
        }
    }

    /**
     * 使用httpclient发送一个post的json请求
     * @param url
     * @return
     */
    public void sendJsonByPost(String json,String url){
        final HttpUtils httpUtils=new HttpUtils();
        RequestParams params = new RequestParams();
        try {
            params.setBodyEntity(new StringEntity(json,"UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        params.setContentType("application/json");
        httpUtils.send(HttpRequest.HttpMethod.POST, url, params, new RequestCallBack<String>() {

            @Override
            public void onFailure(HttpException arg0, String arg1) {
                if(arg0==null)return;
                //这里如果报404异常的话,一般是根据当前的基站cid无法查到相关信息
                //如果返回值是 400  Bad Request,说明有的必填项没有填
                Log.i("AlexLocation","失败了"+arg1+"   "+arg0.getExceptionCode()+"  "+arg0.getMessage(),arg0.getCause());
                if(arg0.getExceptionCode()==0)Log.i("AlexLocation","谷歌没有根据现有条件查询到经纬度");
                if(isDebugging)JViewUtils.showToast(context,"谷歌查询失败",arg1);
            }

            @Override
            public void onSuccess(ResponseInfo<String> arg0) {
                /*
                谷歌返回的json如下
                {
                   "location": {
                    "lat": 1.3553794,
                    "lng": 103.86774439999999
                   },
                   "accuracy": 16432.0
                  }
                 */
                if(arg0==null || context==null)return;
                String result = arg0.result;
                Log.i("AlexLocation","成功"+arg0.result+"   "+arg0.statusCode);
                if(isDebugging)JViewUtils.showToast(context,"谷歌成功",arg0.result);
                if(result==null || result.length()<10 || !result.startsWith("{")){
                    Log.i("AlexLocation","返回格式不对"+result);
                }
                JSONObject returnJson = null;
                try {
                    returnJson = new JSONObject(result);
                    JSONObject location = returnJson.getJSONObject("location");
                    if(location==null){
                        Log.i("AlexLocation","条件不足,无法确定位置");
                        return;
                    }
                    double latitude = location.getDouble("lat");
                    double longitute = location.getDouble("lng");
                    double google_accuracy = returnJson.getDouble("accuracy");
                    Log.i("AlexLocation","谷歌返回的经纬度是"+latitude+"  :  "+longitute+"  精度是"+google_accuracy);
                    if(isDebugging)JViewUtils.showToast(context,"","谷歌返回的经纬度是"+latitude+"  :  "+longitute+"  精度是"+google_accuracy);
                    if(currentStatus==STATUS.NOT_CONNECT || google_accuracy<accuracy){
                        Log.i("Alex","决定采用基站和wifi定位,旧的精确度是"+accuracy);
                        accuracy = (float) google_accuracy;
                        recordLocation(context,latitude,longitute,accuracy);//当没有从GPS获取经纬度成功,或者GPS的获取经纬度精确度不高,则使用基站和wifi的结果
                    }
                } catch (JSONException e) {
                    Log.i("AlexLocation","条件不足,无法确定位置2",e);
                }
            }
        });
    }

    /**
     * 检查当前Wifi网卡状态
     */
    public void checkNetCardState(WifiManager mWifiManager) {
        if (mWifiManager.getWifiState() == 0) {
            Log.i("AlexLocation", "网卡正在关闭");
        } else if (mWifiManager.getWifiState() == 1) {
            Log.i("AlexLocation", "网卡已经关闭");
        } else if (mWifiManager.getWifiState() == 2) {
            Log.i("AlexLocation", "网卡正在打开");
        } else if (mWifiManager.getWifiState() == 3) {
            Log.i("AlexLocation", "网卡已经打开");
        } else {
            Log.i("AlexLocation", "没有获取到状态");
        }
    }

    public static void  getConnectedWifiInfo(WifiManager wifiManager){
        if(wifiManager==null)return;
        WifiInfo wifiConnection = wifiManager.getConnectionInfo();
        if (wifiConnection != null) {//获取当前链接的wifi信息,没什么用
            String wifiMAC = wifiConnection.getBSSID();
            int i = wifiConnection.getRssi();
            String s1 = wifiConnection.getSSID();
            String mac = wifiConnection.getMacAddress();//注意这里的mac是手机的mac而不是热点的mac
            Log.i("AlexLocation","手机的mac地址是"+mac);
        }
    }

    /**
     * 根据经纬度计算两点间的距离
     * @param lat_a
     * @param lng_a
     * @param lat_b
     * @param lng_b
     * @return
     */
    public static double getGPSDistance(double lat_a, double lng_a, double lat_b, double lng_b) {
        final double M_PI = 3.14159265358979323846264338327950288, EARTH_RADIUS = 6378138.0;
        final double dd = M_PI / 180.0;

        double lon2 = lng_b;
        double lat2 = lat_b;

        double x1 = lat_a * dd, x2 = lat2 * dd;
        double y1 = lng_a * dd, y2 = lon2 * dd;
        double distance = (2 * EARTH_RADIUS * Math.asin(Math.sqrt(2 - 2 * Math.cos(x1)
                * Math.cos(x2) * Math.cos(y1 - y2) - 2 * Math.sin(x1)
                * Math.sin(x2)) / 2));
        Log.i("AlexLocation","位置发生了移动,距离是"+distance);
        if(isDebugging && manager!=null)JViewUtils.showToast(manager.context,"位置移动了",distance+"米");
        return distance;
    }
}

附录资料:

移动设备网络代码(英语:Mobile Network Code,MNC)是与移动设备国家代码(Mobile Country Code,MCC)(也称为“MCC / MNC”)相结合,以用来表示唯一一个的移动设备的网络运营商。这些运营商可以是使用的GSM/LTE、CDMA、iDEN、TETRA和通用移动通讯系统的公共陆基移动网亦或是卫星网络。


MCC

MNC

品牌

运营商

使用状态

频段(MHz)

参考和注释
460 00 中国移动

  (China Mobile)

中国移动

  (China Mobile)

营运中 GSM 900 / GSM 1800 / TD-SCDMA 1880 / TD-SCDMA 2010  
460 01 中国联通

  (China Unicom)

中国联通

  (China Unicom)

营运中 GSM 900 / GSM 1800 / UMTS 2100 CDMA网络出售给中国电信,并在2009年5月试商用WCDMA网络,2009年10月开始全面投入商业运行。
460 02 中国移动

  (China Mobile)

中国移动

  (China Mobile)

营运中 GSM 900 / GSM 1800 / TD-SCDMA 1880 / TD-SCDMA 2010  
460 03 中国电信

  (China Telecom)

中国电信

  (China Telecom)

营运中 CDMA2000 800 / CDMA2000 2100 EV-DO
460 05 中国电信

  (China Telecom)

中国电信

  (China Telecom)

营运中    
460 06 中国联通

  (China Unicom)

中国联通

  (China Unicom)

营运中 GSM 900 / GSM 1800 / UMTS 2100  
460 07 中国移动

  (China Mobile)

中国移动

  (China Mobile)

营运中 GSM 900 / GSM 1800 / TD-SCDMA 1880 / TD-SCDMA 010  
460 11 中国电信

  (China Telecom)

中国电信

  (China Telecom)

营运中 CDMA2000 1800 LTE
460 20 中国铁通

  (China Tietong)

中国铁通

  (China Tietong)

营运中 GSM-R  

Location area code:

The served area of a cellular radio network is usually divided into location areas. Location areas are comprised of one or several radio cells. Each location area is given an unique number within the network, the Location Area Code (LAC).

This code is used as a unique reference for the location of a mobile subscriber. This code is necessary to address the subscriber in the case of an incoming call.

The LAC forms part of the Location Area Identifier (LAI) and is broadcasted on the Broadcast Control Channel (BCCH).

电信(CDMA)基站 SID、 BID 、NID参数解释

通过cdma的基站代码确定该基站的经纬度位置,必须知道Sid、Nid、Bid这三个基站数据,缺一不可。

SID(System id)是系统识别码,每个地级市只有一个sid,是唯一的。

NID(Network id)是网络识别码,由各本地网管理,也就是由地级分公司分配。每个地级市可能有1到3个nid。

BID (Basestation id)表示的是网络中的某一个小区,可以理解为基站。

LTE相关:

PCI(physical-layer Cell identity)是由主同步信号(PSS)与辅同步信号(SSS)组成,可以通过简单运算获得。公式如下:PCI=PSS+3*SSS,其中PSS取值为0...2(实为3种不同PSS序列),SSS取值为0...167(实为168种不同SSS序列),利用上述公式可得PCI的范围是从0...503,因此在物理层存在504个PCI。

其实,可以把PCI理解为扰码,就像在WCDMA系统中下行扰码用于区分扇区一样,对待发送的数据进行加扰,以便终端可以区分不同扇区。

而从网络操作维护级别来看,CI(Cell Identity)唯一标识一个小区,在网络中不能重复。但PCI却可以重复,因为PSS+SSS仅有504种组合。如,当网络中有1000个小区时,PCI仅有504个,此时就需要对PCI进行复用,通常情况下,PCI规划原则是每个扇区分配特定的PSS序列(0...2)值,而每个基站分配特定的SSS序列(0...167)值,以此避免相邻基站间存在相同PCI的问题发生。

WCDMA相关:

小区分配主扰码(PSC)。WCDMA系统中下行链路共有512个PSC,每个小区分配一个PSC作为该小区的识别参数之一。当小区的数量超过512个时,可重复分配一个PSC给一个小区,只要保证使用相同PSC的小区之间的距离足够大,使得接收信号在另外一个使用同一PSC的小区覆盖范围内低于门限电平即可。

PSC这个简写在WCDMA中除了上面所描述的Primary scrambling code外,还有另外含义,primary synchronization code,主同步码的含义,用于小区同步。在手机或者无线终端为了实现与小区同步时,先通过这PSC,主同步码,获得信道中信息的时隙同步,然后通过SSC,辅同步码进行帧同步。通过相关操作,匹配滤波器输出的峰值来判断时隙的同步。主同步码在搜有小区都一样,周期10ms,15个时隙(slot),每个时隙中PSC的长度为256chips,15个时隙重复。辅同步码ssc在15个时隙中不一样,通过不同的ssc确定不同的时隙。

时间: 2024-10-24 20:21:52

Android附近基站+Wifi+IP+GPS多渠道定位方案的相关文章

百度定位SDK:弥补Android基站WIFI定位缺失

http://tech.qq.com/a/20120524/000347.htm 如今,基于位置信息的移动应用越来越多,从餐饮.购物等本地生活服务,到定向广告的匹配.移动社交网络的构建,LBS类应用的开发离不开定位功能.国内大多数的地图SDK工具,都提供了免费.精准的定位功能,方便开发者以定位功能为基础,延伸出丰富.交互体验更佳的移动应用. 不过,仅仅是地图定位功能,不少SDK工具也都支持存在着较大差别.最近,一些地图应用的开发者都碰到了这样一个难题,一个由高校学生组织的开发团队,推出了一款LB

彻底解决Android GPS没法定位这一顽固问题

大家去网上搜索Android定位location为null没法定位问题,估计有一大堆文章介绍如何来解决,但是最后大家发现基本没用.本文将从Android定位实现原理来深入分析没法定位原因并提出真正的解决方案.在分析之前,我们肯定得先看看android官方提供的定位SDK. 默认Android GPS定位实例 获取LocationManager: mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVI

多基站wifi混合定位

详情链接:http://www.haoservice.com/docs/3 支持格式: JSON/XML 请求方式: GET/POST 明文方式请求参数:   名称 类型 必填 说明   key string 是 API KEY   requestdata Object 是 参数对象(Json格式),查询数据中至少传入celltowers和wifilist中一组数据         celltowers List 是 基站数据对象数组                 mcc Int 是 mcc国

判断Android的WIFI与GPS状态,并引导用户前去开启GPS与WIFI设置

需要配置权限 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> 判断Android的WIFI与GPS状态 private void checkWifiAndGpsStatus() { boolean res

最新基于高德地图的android进阶开发(3)GPS地图定位

1.下面示例是一个简单的定位,来自官网,对这些源码加了一些注释,这样看起来可能会更容易理解一点. 2.直接上源码 androidManifest.xml <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.dragon.arnav&

Android应用中使用百度地图API定位自己的位置(二)

官方文档:http://developer.baidu.com/map/sdkandev-6.htm#.E7.AE.80.E4.BB.8B3 百度地图SDK为开发者们提供了如下类型的地图覆盖物: 我的位置图层(MyLocationOverlay):用于显示用户当前位置的图层(支持自定义位置图标): Poi搜索结果图层(PoiOverlay):用于显示兴趣点搜索结果的图层: 路线图层(RouteOverlay):公交.步行和驾车线路图层,将公交.步行和驾车出行方案的路线及关键点显示在地图上(起.终

Android手机同时使用Wi-Fi和数据流量

Android手机同时使用Wi-Fi和数据流量 大家都知道,当手机成功连接到Wi-Fi热点以后,手机所产生的上网流量都是通过Wi-Fi来传输的,而手机的移动流量会被禁用.但是,我们现在有特殊的业务需求,需要让手机成功连接Wi-Fi后还可以走数据流量(比如3G.4G). Android手机同时使用Wi-Fi和数据流量 背景介绍 相关调研 查找方法 实现方法 使用wireless-tools方式驱动Wi-Fi 准备工作 通过命令启动Wi-Fi模块 待解决的问题 使用wpa_supplicant方式驱

android 百度地图系列之地图初始化及定位

在Android应用中,很多时候需要地图功能,回顾过去写的项目和百度地图api,开始总结一下Android百度地图的实现.首先总结一下怎么开始一个Android百度地图功能. 当使用百度地图的时候,提到一个appkey."在使用百度地图SDK为您提供的各种LBS能力之前,您需要获取百度地图移动版的开发密钥,该密钥与您的百度账户相关联.因此,您必须先有百度帐户,才能获得开发密钥.并且,该密钥与您创建的过程名称有关.Key地址为:http://lbsyun.baidu.com/apiconsole/

android 5.1 WIFI图标上的感叹号及其解决办法

转自: http://blog.csdn.net/w6980112/article/details/45843129 第一次调试android5.1的 WIFI更改小功能 Wifi 源码的相关路径目录 packages/apps/Settings/src/com/Android/settings/wifi/ frameworks/base/wifi/java/android/net/wifi/ frameworks/base/services/core/java/com/android/serv