運動App後臺持續定位生成軌跡

1. 連續定位採集點

後臺持續定位主要參照高德官網給的示例主要有一下幾點:java

1.定位LocationService,另起進程同時建立守衛進程Service, LocationHelperService,Service掛掉時守衛進程喚起LocationService。
package com.yxc.barchart.map.location.service;

import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import androidx.annotation.Nullable;
import com.yxc.barchart.map.location.util.Utils;

public class LocationHelperService extends Service {
    private Utils.CloseServiceReceiver mCloseReceiver;
    @Override
    public void onCreate() {
        super.onCreate();
        startBind();
        mCloseReceiver = new Utils.CloseServiceReceiver(this);
        registerReceiver(mCloseReceiver, Utils.getCloseServiceFilter());
    }
    @Override
    public void onDestroy() {
        if (mInnerConnection != null) {
            unbindService(mInnerConnection);
            mInnerConnection = null;
        }
        if (mCloseReceiver != null) {
            unregisterReceiver(mCloseReceiver);
            mCloseReceiver = null;
        }
        super.onDestroy();
    }

    private ServiceConnection mInnerConnection;
    private void startBind() {
    final String locationServiceName = "com.yxc.barchart.map.location.service.LocationService";
        mInnerConnection = new ServiceConnection() {
            @Override
            public void onServiceDisconnected(ComponentName name) {
                Intent intent = new Intent();
                intent.setAction(locationServiceName);
                startService(Utils.getExplicitIntent(getApplicationContext(), intent));
            }
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                ILocationServiceAIDL l = ILocationServiceAIDL.Stub.asInterface(service);
                try {
                    l.onFinishBind();
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        };

        Intent intent = new Intent();
        intent.setAction(locationServiceName);
        bindService(Utils.getExplicitIntent(getApplicationContext(), intent), mInnerConnection, Service.BIND_AUTO_CREATE);
    }
  
    private HelperBinder mBinder;
    private class HelperBinder extends ILocationHelperServiceAIDL.Stub{
        @Override
        public void onFinishBind(int notiId) throws RemoteException {
           startForeground(notiId, Utils.buildNotification(LocationHelperService.this.getApplicationContext()));
            stopForeground(true);
        }
    }
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        if (mBinder == null) {
            mBinder = new HelperBinder();
        }
        return mBinder;
    }
}
複製代碼

這裏由於 LocationService、LocationHelperService、主進程分別爲不一樣的進程,因此須要用AIDL經過進程間的交互。android

2.LocationService須要觸發利用notification增長進程優先級
/** * 觸發利用notification增長進程優先級 */
protected void applyNotiKeepMech() {
  startForeground(NOTI_ID, Utils.buildNotification(getBaseContext()));
  startBindHelperService();
}
複製代碼

這裏看Keep等運動App也都是這樣,沒有加在 Android8.0系統的手機上,home鍵到後臺,就沒在定位了,定位listener老是返回緩存的上次定位的Location經緯度。git

3.屏熄斷網電量屏幕

在定位服務中檢測是不是由息屏形成的網絡中斷,若是是,則嘗試進行點亮屏幕。同時,爲了不頻繁點亮,對最小時間間隔進行了設置(能夠按需求修改). 若是息屏沒有斷網,則無需點亮屏幕.github

AMapLocationListener locationListener = new AMapLocationListener() {
        @Override
        public void onLocationChanged(AMapLocation aMapLocation) {
            //發送結果的通知
            sendLocationBroadcast(aMapLocation);
	   			 //判斷是否須要對息屏斷wifi的狀況進行處理
            if (!mIsWifiCloseable) {
                return;
            }
	    			//將定位結果和設備狀態一塊兒交給mWifiAutoCloseDelegate
            if (aMapLocation.getErrorCode() == AMapLocation.LOCATION_SUCCESS) {
                //...
            } else {
               //...
            }
        }
        private void sendLocationBroadcast(AMapLocation aMapLocation) {
            //記錄信息併發送廣播...
        }
    };

/** 處理息屏後wifi斷開的邏輯*/
public class WifiAutoCloseDelegate implements IWifiAutoCloseDelegate {
    /** * 請根據後臺數據自行添加。此處只針對小米手機 * @param context * @return */
    @Override
    public boolean isUseful(Context context) {
       //...
    }

    /** 因爲服務可能被殺掉,因此在服務初始化時,初始相關參數*/
    @Override
    public void initOnServiceStarted(Context context) {
        //...
    }

    /** 處理定位成功的信息*/s
    @Override
    public void onLocateSuccess(Context context, boolean isScreenOn, boolean isMobileable) {
        //...
    }
	
    /** 處理定位失敗的信息。若是須要喚醒屏幕,則嘗試喚醒*/
    @Override
    public void onLocateFail(Context context, int errorCode, boolean isScreenOn, boolean isWifiable) {
        //...
    }
}
複製代碼

點亮屏幕時,會利用最小間隔時間加以限制數據庫

/** * 喚醒屏幕 */
    public void wakeUpScreen(final Context context) {
        try {
            acquirePowerLock(context, PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.SCREEN_DIM_WAKE_LOCK);
        } catch (Exception e) {
            throw e;
        }
    }


/** * 根據levelAndFlags,得到PowerManager的WaveLock * 利用worker thread去得到鎖,以避免阻塞主線程 * @param context * @param levelAndFlags */
    private void acquirePowerLock(final Context context, final int levelAndFlags) {
        if (context == null) {
            throw new NullPointerException("when invoke aquirePowerLock , context is null which is unacceptable");
        }

        long currentMills = System.currentTimeMillis();

        if (currentMills - mLastWakupTime < mMinWakupInterval) {
            return;
        }
        mLastWakupTime = currentMills;

        if (mInnerThreadFactory == null) {
            mInnerThreadFactory = new InnerThreadFactory();
        }
        mInnerThreadFactory.newThread(new Runnable() {
            @Override
            public void run() {
                if (pm == null) {
                    pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
                }

                if (pmLock != null) { // release
                    pmLock.release();
                    pmLock = null;
                }

                pmLock = pm.newWakeLock(levelAndFlags, "MyTag");
                pmLock.acquire();
                pmLock.release();
            }
        }).start();
    }
複製代碼

以上涉及到幾個權限:緩存

<!--容許程序訪問CellID或WiFi熱點來獲取粗略的位置-->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
複製代碼

6.0以上須要本身處理動態權限。網絡

2. 存儲Location採集點

1.對GPS採集到的點進行加工處理數據結構

AMapLocationListener locationListener = new AMapLocationListener() {
        @Override
        public void onLocationChanged(AMapLocation aMapLocation) {
          //有時候會返回(0,0)點須要排除
            if (aMapLocation.getLatitude() == 0f || aMapLocation.getLongitude() <= 0.001f) {
                return;
            }
         //計算跟上一個點的距離,當距離不變時或者變化過小時視爲同一點,不在inter新的點,而是修改當前點的endTime以及duration。
         double itemDistance = LocationComputeUtil.getDistance(aMapLocation, lastSaveLocation);
            if (lastSaveLocation == null && aMapLocation.getLatitude() > 0f) {
                //record的第一個埋點,插入數據庫
                lastSaveLocation = aMapLocation;
            } else if (itemDistance > 1.0f) {
                resetIntervalTimes(0);//新的點
                lastSaveLocation = aMapLocation;
            } else {//可能在原地打點,不存入新數據,update endTime。
                long timestamp = lastSaveLocation.getTime();
                long endTime = System.currentTimeMillis();//todo 須要考慮定位時間跟系統時間的差值。
                long duration = endTime - timestamp;
                resetIntervalTimes(duration);
            }
            sendLocationBroadcast(aMapLocation);
            //發送結果的通知
            .....
        }
複製代碼

//計算跟上一個點的距離,當距離不變時或者變化過小時視爲同一點,不在inter新的點,而是修改當前點的endTime以及duration。併發

第一個點直接存儲。存儲原始點的數據結構:app

Timestamp endTime duration Latitude Longitude speed itemDistance distance locationStr recordType recordId milePost

recordType:運動記錄類型

recordId: 運動記錄Id (經過查找同一 ID以及 recordType下的全部點,而後繪製運動軌跡)

itemDistance 爲跟上一個存儲點的距離,distance表示跟起始點的距離,能夠用來標記里程碑,milePost用來標記里程碑點。endTime、duration能夠用來標誌該位置停留的時間。

locationStr:包含了當前點的latitude、longitude等重要字段,可供反向解析成AMapLocation。

resetIntervalTimes(duration)方法根據duration的時間長短有各自不一樣的策略:好比調整定位採集Location的頻率,或者到某一時間段就直接中止LocationService

//LocationService

private long intervalTime = LocationConstants.DEFAULT_INTERVAL_TIME;
     private void resetIntervalTimes(long duration) {
       if (duration >= 60 * 60 * 1000){// 90分鐘中止本身的服務, 應該還要關閉守護進程
         onDestroy();
         return;
       }
       int intervalTimes = LocationComputeUtil.computeIntervalTimes(duration);
       intervalTime = intervalTimes * LocationConstants.DEFAULT_INTERVAL_TIME;
       mLocationOption.setInterval(intervalTime);
       mLocationClient.setLocationOption(mLocationOption);
  }


 public static int computeIntervalTimes(long duration) {
        long timeMin = 60 * 1000;
        if (duration > timeMin) {
            return 2;
        } else if (duration > 4 * timeMin) {
            return 3;
        } else if (duration > 10 * timeMin) {
            return 5;
        }
        return 1;
  }
複製代碼

這裏其實能夠當duration大於某個時間段值時就stopservice,而後經過陀螺儀、計步器等其餘手段來從新喚起GPS定位。

以上是LocationService採集點,由於它在remote1的進程中,因此沒有在這裏進行DB的insert以及update的操做,而是發出廣播給LocalLocationService,一下是LocalLocationService收到broadcast時,數據存儲過程:

//LocalLocationService


public void onLocationChanged(AMapLocation aMapLocation) {
  if (aMapLocation.getLatitude() == 0f || aMapLocation.getLongitude() <= 0.001f) {
    return;
  }
  //計算當前定位點跟上一保存點的距離爲itemDistance,當lastSaveLocation爲null時itemDistance爲0.
  double itemDistance = LocationComputeUtil.getDistance(aMapLocation, lastSaveLocation);
  if (lastSaveLocation == null && aMapLocation.getLatitude() > 0f) {
    //record的第一個埋點,插入數據庫
    Log.d("LocationService", "第一個點。。。");
    Toast.makeText(LocalLocationService.this, "Service first insert Point",        Toast.LENGTH_SHORT).show();
    LocationDBHelper.deleteRecordLocationList(recordType, recordId);
    String locationStr = LocationComputeUtil.amapLocationToString(aMapLocation);
    double distance = 0;
    double milePost = 0;
    RecordLocation recordLocation = RecordLocation.createLocation(aMapLocation, recordId, recordType, itemDistance, distance, locationStr, milePost);
    //首個點直接insert到數據庫
    LocationDBHelper.insertRecordLocation(recordLocation);

    Log.d("LocationService", "first insert recordLocation:" + recordLocation.toString());
    sendEventbus(aMapLocation, recordLocation);
    //有insert操做時就更新當前Location爲 lastSaveLocation點,供下次計算使用。
    lastSaveLocation = aMapLocation;
    lastRecordLocation = recordLocation;
  } else if (itemDistance > 1.0f) {
    Toast.makeText(LocalLocationService.this, "save Point:" + aMapLocation.getLatitude(), Toast.LENGTH_SHORT).show();
    String locationStr = LocationComputeUtil.amapLocationToString(aMapLocation);
    if (lastRecordLocation != null) {
      //根據上一個保存Location值累計計算 當前點的 distance值。
      double distance = lastRecordLocation.distance + itemDistance;
      double milePost = 0;
      if (distance >= mMilePost){//當恰好大於里程碑點時就記錄該點爲里程碑點,並修改里程碑值,等待記錄下一里程碑點。
        milePost = mMilePost;
        mMilePost += LocationConstants.MILE_POST_ONE_KILOMETRE;
      }
      RecordLocation recordLocation = RecordLocation.createLocation(aMapLocation, recordId, recordType,itemDistance, distance, locationStr, milePost);
      //當前Location的time 跟 上個點的endTime 同 itemDistance計算獲得當前點的Speed值。
      long time = (aMapLocation.getTime() - lastRecordLocation.endTime)/1000;
      float speed = (float) (itemDistance * 1.0f/time);
      recordLocation.speed = speed;
      lastRecordLocation = recordLocation;
      //insert
      LocationDBHelper.insertRecordLocation(recordLocation);

      //修改lastSaveLocation的endTime, duration。
      // long lastSaveLocationEndTime = aMapLocation.getTime();
      //long lastSaveLocationDuration = aMapLocation.getTime() - lastSaveLocation.getTime(); //LocationDBHelper.updateRecordLocation(lastSaveLocation.getTime(),lastSaveLocationEndTime, 
      //lastSaveLocationDuration);
      sendEventbus(aMapLocation, recordLocation);
      Log.d("LocationService", "insert recordLocation:" + recordLocation.toString());
    }
    lastSaveLocation = aMapLocation;
  } else {//可能在原地打點,不存入新數據,update endTime。
    Toast.makeText(LocalLocationService.this, "update Point:" + aMapLocation.getLatitude(), Toast.LENGTH_SHORT).show();
    long timestamp = lastSaveLocation.getTime();
    long endTime = System.currentTimeMillis();//todo 須要考慮定位時間跟系統時間的差值。
    long duration = endTime - timestamp;
    //更新LastSaveLocation的 endTime,duration值
    LocationDBHelper.updateRecordLocation(timestamp, endTime, duration);
  }
}
複製代碼

LocalLocationService跟LocationService同樣分三種狀況:

1.首次記錄 直接create RecordLocation而後 insert到數據庫

  1. Location變化時插入新的點;根據保存的 lastSaveLocation計算新的變化的Location點ItemDistance, distance,speed, 以及看它是不是里程碑點,寫入milePost字段。具體操做參考以上代碼註釋。
  2. 變化範圍小時,視爲當前點修改endtime、duration。

當有新的數據插入時,就更新UI,經過EventBus傳遞RecordLocation

private void sendEventbus(AMapLocation aMapLocation, RecordLocation recordLocation) {
        //改爲發送eventBus
      EventBus.getDefault().post(new LocationEvent(aMapLocation, recordLocation));
}

複製代碼

3. 繪製RecordPath

LocationActivity收到EventBus時,可使用EventBus傳過來的數據,也能夠從DB中拿取點進行實時的繪製。

//eventBus 接受 LocalService 傳過來的數據
    @Subscribe
    public void onLocationSaved(LocationEvent locationEvent){
        if (locationEvent.mapLocation != null){
            onLocationChanged(locationEvent.mapLocation, locationEvent.recordLocation);
        }
    }
複製代碼

這裏我是從數據庫裏面拿取點,經過查詢 timestamp greaterThan 來獲取LocationList,而後進行繪製軌跡。

/** * 定位結果回調 * * @param amapLocation 位置信息類 */
public void onLocationChanged(AMapLocation amapLocation, RecordLocation sendRecordLocation) {
  if (amapLocation != null && amapLocation.getErrorCode() == 0) {
    if (lastLocation != null) {
      long timestamp = lastLocation.getTime();
      //從數據庫裏拿點
     List<RecordLocation> locationList 
       = LocationDBHelper.getLateLocationList(recordId, timestamp);
      record.addPointList(locationList);
      for (int i = 0; i < locationList.size(); i++) {
       RecordLocation recordLocation = locationList.get(i);
       AMapLocation aMapLocation=LocationComputeUtil.parseLocation(recordLocation.locationStr);
       LatLng myLocation = new LatLng(aMapLocation.getLatitude(), aMapLocation.getLongitude());
       mPolyOptions.add(myLocation);
      }
      redRawLine();
    } else {//第一個點直接繪製
      LatLng myLocation = new LatLng(amapLocation.getLatitude(), amapLocation.getLongitude());
      mAMap.moveCamera(CameraUpdateFactory.changeLatLng(myLocation));
      if (btn.isChecked()) {
        Log.d("Location", "record " + myLocation);
        record.addPoint(sendRecordLocation);
        mPolyOptions.add(myLocation);
        redRawLine();
      }
    }
    lastLocation = amapLocation;
  } else {
    String errText = "定位失敗," + amapLocation.getErrorCode() + ": "
      + amapLocation.getErrorInfo();
    Log.e("AmapErr", errText);
  }
}
複製代碼

繪製實時軌跡

/** * 實時軌跡畫線 */
private void redRawLine() {
  if (mPolyOptions.getPoints().size() > 1) {
    if (mPolyline != null) {
      mPolyline.setPoints(mPolyOptions.getPoints());
    } else {
      mPolyline = mAMap.addPolyline(mPolyOptions);
    }
  }
}
複製代碼

保存軌跡路徑到Model Record中,而且存入數據庫:

if (!TextUtils.isEmpty(recordId)){
  	//根據recordType, recordId查詢原始LocationList
     List<RecordLocation> locationList = LocationDBHelper.getLocationList(recordType,recordId);
     saveRecord(locationList);
 }
//查詢全部點,而後生成Record對象,插入Record對象到DB

protected void saveRecord(List<RecordLocation> list) {
  if (list != null && list.size() > 0) {
    RecordLocation firstLocation = list.get(0);
    RecordLocation lastLocation = list.get(list.size() - 1);
    double distance = lastLocation.distance;
    long duration = getDuration(firstLocation, lastLocation);
    String averageSpeed = getAverage(distance, duration);
    String pathLineStr = LocationComputeUtil.getPathLineStr(list);
    String dateStr = TimeDateUtil.getDateStrMinSecond(firstLocation.getTimestamp());

    Record record = Record.createRecord(recordType, Double.toString(distance),Long.toString(duration), averageSpeed, pathLineStr,firstLocation.locationStr,lastLocation.locationStr, dateStr);
    LocationDBHelper.insertRecord(record);
  } else {
    Toast.makeText(LocationActivity.this, "沒有記錄到路徑", Toast.LENGTH_SHORT).show();
  }
}

private long getDuration(RecordLocation firstLocation, RecordLocation lastLocation) {
  return (lastLocation.getEndTime() - firstLocation.getTimestamp()) / 1000;
}

private String getAverage(double distance, long duration) {
  return String.valueOf(distance/duration);
}
複製代碼

Record是跟估計路徑相關的Model,包含了全部Location點的數據,經過Gson 轉化 數據庫查詢出來的LocationList轉化而來,這裏開發過程當中遇到點問題:我用的數據庫是Realm,數據庫查詢出來的是RecordLocationProxy對象,而並不是RecordLocation對象致使Gson轉換出問題,因此我這裏直接將RecordLocationProxy 進行DeepClone成 RecordLocation,而後這裏是一個List,處理每一個對象的每一個字段,效率不是很好,暫時沒有想到好的方案。

同時生成的Gson對象也須要對RealmObject的字段進行過濾等操做,關於Realm數據庫相關的知識不是本篇的着重點,因此不作介紹。到時看可否找到解決以上問題的方法時單獨來寫介紹。

public class Record extends RealmObject {
  @PrimaryKey
  public int id;
  public int recordType;
  public String distance;
  public String duration;
  public String speed;//這裏是averageSpeed
  public String pathLine;//全部點的Gson字符串。
  @Ignore
  private AMapLocation mStartPoint;
  @Ignore
  private AMapLocation mEndPoint;
  public String startPoint;//起始點的重要字段字符串,對應RecordLocation的LocationStr
  public String endPoint;//結束點的重要字段字符串,對應RecordLocation的LocationStr
  public String date;//起始點對應Timestamp的dateStr

  @Ignore
  public List<RecordLocation> mPathLocationList = new ArrayList<>();
  public Record() {
  }

  public static Record createRecord(int recordType, String distance, String duration, String speed, String pathLine, String startPoint,String endPoint, String date) {
    Record record = new Record();
    record.recordType = recordType;
    record.distance = distance;
    record.duration = duration;
    record.speed = speed;
    record.pathLine = pathLine;
    record.startPoint = startPoint;
    record.endPoint = endPoint;
    record.date = date;
    return record;
  }
  ......
}
複製代碼

RecordLocation單點的數據結構,對應以上介紹過的數據表:

public class RecordLocation extends RealmObject {

    @PrimaryKey
    public long timestamp;//時間戳
    public long endTime;//當前點待了多久,用 endTime - timestamp = duration。
    public long duration;
    public double longitude;//精度
    public double latitude;//維度
    public float speed;//單點的速度,用來劃線的時候上不一樣的顏色
    public double itemDistance;//距離上一個點的距離
    public double distance;//距離起始點的距離
    public String recordId;//運動記錄 id(用於聚合查詢)
    public int recordType;//運動類型,跑步,騎行,駕駛。
    public String locationStr;//包含AMapLocation的字段
    public double milePost;//里程碑

    public RecordLocation() {

    }

    public static RecordLocation copyLocation(RecordLocation originalLocation){
        RecordLocation recordLocation = new RecordLocation();
        recordLocation.timestamp = originalLocation.getTimestamp();
        recordLocation.endTime = originalLocation.getEndTime();
        recordLocation.duration = originalLocation.getDuration();
        recordLocation.latitude = originalLocation.getLatitude();
        recordLocation.longitude = originalLocation.getLongitude();
        recordLocation.speed = originalLocation.getSpeed();
        recordLocation.recordId = originalLocation.getRecordId();
        recordLocation.recordType = originalLocation.getRecordType();
        recordLocation.itemDistance = originalLocation.getItemDistance();
        recordLocation.distance = originalLocation.getDistance();
        recordLocation.locationStr = originalLocation.getLocationStr();
        recordLocation.milePost=originalLocation.getMilePost();
        return recordLocation;
    }

    public static RecordLocation createLocation(AMapLocation location, String recordId, int recordType, double itemDistance, double distance, String locationStr, double milePost){
        RecordLocation recordLocation = new RecordLocation();
        recordLocation.timestamp = location.getTime();
        recordLocation.endTime = recordLocation.timestamp;
        recordLocation.duration = 0;
        recordLocation.latitude = location.getLatitude();
        recordLocation.longitude = location.getLongitude();
        recordLocation.speed = location.getSpeed();
        recordLocation.recordId = recordId;
        recordLocation.recordType = recordType;
        recordLocation.itemDistance = itemDistance;
        recordLocation.distance = distance;
        recordLocation.locationStr = locationStr;
        recordLocation.milePost = milePost;
        return recordLocation;
    }

    @Override
    public String toString() {
        return "RecordLocation{" +
                "timestamp=" + timestamp +
                ", endTime=" + endTime +
                ", duration=" + duration +
                ", longitude=" + longitude +
                ", latitude=" + latitude +
                ", speed=" + speed +
                ", itemDistance=" + itemDistance +
                ", distance=" + distance +
                ", recordId='" + recordId + '\'' +
                ", recordType=" + recordType +
                ", locationStr='" + locationStr + '\'' +
                ", milePost=" + milePost +
                '}';
    }
  
  。。。。
}
複製代碼

總結

數據採集,存儲,繪製流程不復雜,測試過程比較繁瑣,須要到外頭採集GPS點;調試起來相對而言就不是很方便,以前Android8.0以上的後天,熄屏等形成的不能連續打點的問題;以及採集到數據後,不能一目瞭然的看到原始數據等諸多阻礙流程的問題在堅持不懈的努力下獲得解決。這裏的原始數據查看,我是用的Realm的 工具Reaml Browser查看數據庫文件 **.remal文件的方法進行,仍是蠻方便的。這裏我設置了Realm的Config直接存儲文件到SD卡下,不過這裏有個權限的問題就是,RealmConfig的配置指定SD卡讀寫是在Application onCreate中,6.0以上的動態權限尚未來得及申請,一樣這個問題之後留到Realm單獨深究之後作介紹。

最後附一張打點的圖,須要源碼的朋友能夠留言聯繫我,暫時Github須要整理後才能給出來。

相關文章
相關標籤/搜索