/* * Copyright (C) 2015-present, Ant Financial Services Group * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.alipay.hulu.screenRecord; import android.annotation.TargetApi; import android.app.Notification; import android.app.Service; import android.content.Context; import android.content.Intent; import android.graphics.Rect; import android.media.MediaCodecInfo; import android.media.projection.MediaProjection; import android.media.projection.MediaProjectionManager; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.ImageView; import android.widget.ListView; import android.widget.SimpleAdapter; import android.widget.TextView; import com.alipay.hulu.R; import com.alipay.hulu.activity.MyApplication; import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.injector.InjectorService; import com.alipay.hulu.common.injector.param.Subscriber; import com.alipay.hulu.common.injector.provider.Param; import com.alipay.hulu.common.tools.BackgroundExecutor; import com.alipay.hulu.common.tools.CmdLine; import com.alipay.hulu.common.tools.CmdTools; import com.alipay.hulu.common.utils.FileUtils; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.MiscUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.event.EventService; import com.alipay.hulu.shared.event.bean.UniversalEventBean; import com.alipay.hulu.shared.event.constant.Constant; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @TargetApi(value = Build.VERSION_CODES.LOLLIPOP) public class RecordService extends Service { public static final String INTENT_RESULT_CODE = "INTENT_RESULT_CODE"; public static final String INTENT_VIDEO_CODEC = "INTENT_VIDEO_CODEC"; public static final String INTENT_WIDTH = "INTENT_WIDTH"; public static final String INTENT_HEIGHT = "INTENT_HEIGHT"; public static final String INTENT_FRAME_RATE = "INTENT_FRAME_RATE"; public static final String INTENT_VIDEO_BITRATE = "INTENT_VIDEO_BITRATE"; public static final String INTENT_EXCEPT_DIFF = "INTENT_EXCEPT_DIFF"; public static final String ACTION_INIT = "ACTION_INIT"; private static final String TAG = RecordService.class.getSimpleName(); private static final String VIDEO_DIR = "ScreenCaptures"; private WindowManager wm = null; private WindowManager.LayoutParams wmParams = null; private static int NOTIFICATION_ID = 1313; private View view; private TextView recordBtn; private ImageView closeBtn; private ListView resultList; private TextView killCurrent; private SimpleAdapter adapter; private ImageView resultHide; private float mTouchStartX; private float mTouchStartY; private float x; private float y; int state, lastState; private int statusBarHeight = 0; private long lastMotionDownTime; private List<Long> results; private List<Map<String, String>> displayDataSource; private String mCodec; private int mFrameRate; private int mBitrate; private int mWidth; private int mHeight; private boolean isRecording; private MediaProjectionManager mMediaProjectionManager; private ScreenRecorder mRecorder; private Notifications mNotifications; private String lastVideoPath; private long lastRecorderStartTime; private long lastCalculateT1; private boolean hasClicked = false; private boolean isCalculating = false; private VideoEncodeConfig mVideo; private boolean hideResult = false; private Handler mHandler; private MediaProjection mMediaProjection; private InjectorService injectorService; private EventService eventService; private Intent mIntent; private int mResultCode; private double mExceptDiff; @Override public void onCreate() { super.onCreate(); LogUtil.d(TAG, "onCreate"); results = new ArrayList<>(); createView(); mMediaProjectionManager = (MediaProjectionManager)getSystemService(Context.MEDIA_PROJECTION_SERVICE); mNotifications = new Notifications(getApplicationContext()); mHandler = new Handler(); injectorService = LauncherApplication.getInstance().findServiceByName(InjectorService.class.getName()); injectorService.register(this); eventService = LauncherApplication.getInstance().findServiceByName(EventService.class.getName()); eventService.startTrackTouch(); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } private void createView() { view = LayoutInflater.from(this).inflate(R.layout.record_service, null); recordBtn = (TextView) view.findViewById(R.id.record_btn); recordBtn.setText(R.string.record__start_record); closeBtn = (ImageView) view.findViewById(R.id.close_btn); resultList = (ListView) view.findViewById(R.id.record_session_result); killCurrent = (TextView) view.findViewById(R.id.record_kill_current); resultHide = (ImageView) view.findViewById(R.id.record_session_hide); displayDataSource = new ArrayList<>(); adapter = new SimpleAdapter(this, displayDataSource, R.layout.item_screen_result, new String[] {"title", "value"}, new int[] {R.id.screen_result_title, R.id.screen_result_value}); resultList.setAdapter(adapter); resultList.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (position < results.size()) { removeResultAt(position); } else { clearResult(); } } }); killCurrent.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { BackgroundExecutor.execute(new Runnable() { @Override public void run() { if (CmdTools.isInitialized()) { String[] pA = CmdTools.getTopPkgAndActivity(); if (pA == null || pA.length != 2) { LauncherApplication.getInstance().showToast("获取当前应用失败"); return; } LogUtil.i(TAG, "当前应用: %s, 当前Activity: %s", pA[0], pA[1]); // 杀两遍 CmdTools.execHighPrivilegeCmd("am force-stop " + pA[0]); CmdTools.execHighPrivilegeCmd("am force-stop " + pA[0]); } else { // 申请ADB requestAdb(); } } }); } }); resultHide.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { hideResult = !hideResult; if (hideResult) { resultList.setVisibility(View.GONE); resultHide.setRotation(0); } else { resultList.setVisibility(View.VISIBLE); resultHide.setRotation(180); } } }); if (statusBarHeight == 0) { try { Class<?> clazz = Class.forName("com.android.internal.R$dimen"); Object object = clazz.newInstance(); statusBarHeight = Integer.parseInt(clazz.getField("status_bar_height") .get(object).toString()); statusBarHeight = getResources().getDimensionPixelSize(statusBarHeight); } catch (Exception e) { e.printStackTrace(); } finally { if (statusBarHeight == 0) { statusBarHeight = 50; } } } wm = (WindowManager) getApplicationContext().getSystemService(WINDOW_SERVICE); wmParams = ((MyApplication)getApplication()).getFloatWinParams(); wmParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; wmParams.flags |= 8; wmParams.gravity = Gravity.LEFT | Gravity.TOP; // 调整悬浮窗口至左上角 // 以屏幕左上角为原点,设置x、y初始值 wmParams.x = 0; wmParams.y = 0; // 设置悬浮窗口长宽数据 wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT; wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT; wmParams.format = 1; wmParams.alpha = 1f; wm.addView(view, wmParams); final Rect closeRect = new Rect(); final Rect recordRect = new Rect(); view.postDelayed(new Runnable() { @Override public void run() { closeBtn.getHitRect(closeRect); recordBtn.getGlobalVisibleRect(recordRect); } }, 500); view.setOnTouchListener(new View.OnTouchListener() { public boolean onTouch(View v, MotionEvent event) { // 获取相对屏幕的坐标,即以屏幕左上角为原点 x = event.getRawX(); y = event.getRawY() - statusBarHeight; LogUtil.i(TAG, "currX" + x + "====currY" + y); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: state = MotionEvent.ACTION_DOWN; // 获取相对View的坐标,即以此View左上角为原点 mTouchStartX = event.getX(); mTouchStartY = event.getY(); LogUtil.i(TAG, "startX" + mTouchStartX + "====startY" + mTouchStartY); lastState = state; lastMotionDownTime = System.currentTimeMillis(); break; case MotionEvent.ACTION_MOVE: state = MotionEvent.ACTION_MOVE; updateViewPosition(); lastState = state; break; case MotionEvent.ACTION_UP: state = MotionEvent.ACTION_UP; if (System.currentTimeMillis() - lastMotionDownTime < ViewConfiguration.getTapTimeout()) { float curX = event.getX(); float curY = event.getY(); if (closeRect.contains((int)curX, (int)curY) && closeRect.contains((int)mTouchStartX, (int)mTouchStartY)) { LogUtil.i(TAG, "Click Close Btn"); onCloseBtnClicked(); } else if (recordRect.contains((int)curX, (int)curY) && recordRect.contains((int)mTouchStartX, (int)mTouchStartY)) { LogUtil.i(TAG, "Click Record Btn"); onRecordBtnClicked(); } } updateViewPosition(); mTouchStartX = mTouchStartY = 0; lastState = state; break; } return false; } }); view.setAlpha(0.8f); } private void requestAdb() { LauncherApplication.getInstance().showDialog(RecordService.this, "ADB连接尚未开启,是否开启?", "开启", new Runnable() { @Override public void run() { BackgroundExecutor.execute(new Runnable() { @Override public void run() { boolean result; try { result = CmdTools.generateConnection(); } catch (Exception e) { LogUtil.e(TAG, "连接adb异常", e); result = false; } if (result) { LauncherApplication.getInstance().showToast("开启成功"); } else { LauncherApplication.getInstance().showToast("开启失败"); } } }); } }, "取消", null); } private void onRecordBtnClicked() { if (isCalculating) { return; } LogUtil.w("yuawen", "上一次点击开始/结束录制的时间:" + System.currentTimeMillis()); if (isRecording) { stopRecorder(); } else { if (initRecorder()) { startRecorder(); } } } private void onCloseBtnClicked() { stopRecorder(); stopSelf(); } private void updateViewPosition() { // 更新浮动窗口位置参数 wmParams.x = (int) (x - mTouchStartX); wmParams.y = (int) (y - mTouchStartY); wmParams.alpha = 1F; wm.updateViewLayout(view, wmParams); } @Override public int onStartCommand(Intent intent, int flags, int startId) { LogUtil.d(TAG, "onStart"); Notification notification = new Notification.Builder(this).setContentText(getString(R.string.float__toast_title)).setSmallIcon(R.drawable.solopi_main).build(); startForeground(NOTIFICATION_ID, notification); if (intent == null) { return super.onStartCommand(intent, flags, startId); } if (ACTION_INIT.equals(intent.getAction())) { mResultCode = intent.getIntExtra(INTENT_RESULT_CODE, 0); mIntent = intent; mCodec = intent.getStringExtra(INTENT_VIDEO_CODEC); mFrameRate = intent.getIntExtra(INTENT_FRAME_RATE, 0); mBitrate = intent.getIntExtra(INTENT_VIDEO_BITRATE, 0); mWidth = intent.getIntExtra(INTENT_WIDTH, 0); mHeight = intent.getIntExtra(INTENT_HEIGHT, 0); mExceptDiff = intent.getDoubleExtra(INTENT_EXCEPT_DIFF, 0); } return super.onStartCommand(intent, flags, startId); } private boolean initRecorder() { try { mMediaProjection = mMediaProjectionManager.getMediaProjection(mResultCode, mIntent); if (mMediaProjection == null) { LogUtil.e(TAG, "media projection is null"); stopSelf(); return false; } mVideo = createVideoConfig(); if (mVideo == null) { mMediaProjection.stop(); stopSelf(); return false; } File record = FileUtils.getSubDir(VIDEO_DIR); if (!record.exists()) { stopRecorder(); stopSelf(); return false; } LogUtil.i(TAG, "video dir is: " + record.getAbsolutePath()); LogUtil.i(TAG, "is video dir exists?" + record.exists()); mRecorder = createRecorder(mMediaProjection, mVideo, generateVideoPath()); return true; } catch (Exception e) { LogUtil.e(TAG, e.getMessage(), e); return false; } } @NonNull private File generateVideoPath() { SimpleDateFormat format = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US); final File file = new File(FileUtils.getSubDir(VIDEO_DIR), "Screen-" + format.format(new Date()) + "-" + mVideo.width + "x" + mVideo.height + ".mp4"); LogUtil.d(TAG, "Create recorder with :" + mVideo + " \n " + file); lastVideoPath = file.getAbsolutePath(); if (mRecorder != null) { mRecorder.updateDstPath(lastVideoPath); } return file; } /** * 增加结果列 * @param result */ private void addResultValue(long result) { results.add(result); displayDataSource.clear(); long total = 0; for (int i = 0; i < results.size(); i++) { long val = results.get(i); total += val; Map<String, String> display = new HashMap<>(3); display.put("title", "第" + (i + 1) + "次"); display.put("value", val + "ms"); displayDataSource.add(display); } Map<String, String> display = new HashMap<>(3); display.put("title", "平均值"); display.put("value", (total / results.size()) + "ms"); displayDataSource.add(display); LauncherApplication.getInstance().runOnUiThread(new Runnable() { @Override public void run() { adapter.notifyDataSetChanged(); } }); } /** * 清空结果列 */ private void clearResult() { results.clear(); displayDataSource.clear(); LauncherApplication.getInstance().runOnUiThread(new Runnable() { @Override public void run() { adapter.notifyDataSetChanged(); } }); } /** * 删除结果列特定项 */ private void removeResultAt(int position) { if (position >= results.size() || position < 0) { return; } results.remove(position); displayDataSource.clear(); long total = 0; for (int i = 0; i < results.size(); i++) { long val = results.get(i); total += val; Map<String, String> display = new HashMap<>(3); display.put("title", "第" + (i + 1) + "次"); display.put("value", val + "ms"); displayDataSource.add(display); } if (results.size() > 0) { Map<String, String> display = new HashMap<>(3); display.put("title", "平均值"); display.put("value", (total / results.size()) + "ms"); displayDataSource.add(display); } LauncherApplication.getInstance().runOnUiThread(new Runnable() { @Override public void run() { adapter.notifyDataSetChanged(); } }); } @Override public void onDestroy() { LogUtil.d(TAG, "onDestroy"); wm.removeView(view); stopForeground(true); injectorService.unregister(this); injectorService = null; // 停止监听 eventService.stopTrackTouch(); LauncherApplication.getInstance().stopServiceByName(EventService.class.getName()); eventService = null; super.onDestroy(); } private ScreenRecorder createRecorder(MediaProjection mediaProjection, final VideoEncodeConfig video , final File output) { ScreenRecorder r = new ScreenRecorder(video, 1, mediaProjection, output.getAbsolutePath()); r.setCallback(new ScreenRecorder.Callback() { long startTime = 0; @Override public void onStop(Throwable error) { mHandler.post(new Runnable() { @Override public void run() { isRecording = false; isCalculating = true; recordBtn.setText(R.string.record__calculating); LauncherApplication.getInstance().showToast(getString(R.string.record__please_wait)); } }); BackgroundExecutor.execute(new Runnable() { @Override public void run() { VideoAnalyzer.getInstance().doAnalyze(lastCalculateT1,video.exceptDiff , lastVideoPath, new VideoAnalyzer.AnalyzeListener() { @Override public void onAnalyzeFinished(final long result) { mHandler.post(new Runnable() { @Override public void run() { isCalculating = false; recordBtn.setText(R.string.record__start_record); if (result <= 0) { LauncherApplication.getInstance().showToast(getString(R.string.record__operation_fast)); } else { addResultValue(result); } } }); } @Override public void onAnalyzeFailed(final String msg) { mHandler.post(new Runnable() { @Override public void run() { isCalculating = false; recordBtn.setText(R.string.record__start_record); LauncherApplication.getInstance().showToast(msg); } }); } }); } }, 2000); if (error != null) { LogUtil.e(TAG, "stop record is error now... error msg:\n" + MiscUtil.stackTraceToString(error.getStackTrace())); output.delete(); } } @Override public void onStart() { lastRecorderStartTime = System.currentTimeMillis(); LogUtil.w("yuawen", "录屏开始时间:" + lastRecorderStartTime); mHandler.post(new Runnable() { @Override public void run() { isRecording = true; hasClicked = false; recordBtn.setText(R.string.record__stop_record); mNotifications.recording(0); } }); } @Override public void onRecording(long presentationTimeUs) { if (startTime <= 0) { startTime = presentationTimeUs; } long time = (presentationTimeUs - startTime) / 1000; mNotifications.recording(time); } }); return r; } private void startRecorder() { if (mRecorder == null) { return; } mRecorder.start(); } private void stopRecorder() { mNotifications.clear(); if (mRecorder != null) { mRecorder.quit(); } mRecorder = null; } private VideoEncodeConfig createVideoConfig() { final String codec = mCodec; if (codec == null) { return null; } int width = mWidth; int height = mHeight; int framerate = mFrameRate; int iframe = 1; int bitrate = mBitrate; double exceptDiff = mExceptDiff; MediaCodecInfo.CodecProfileLevel profileLevel = null; return new VideoEncodeConfig(width, height, bitrate, framerate, iframe, codec, ScreenRecorder.VIDEO_AVC, profileLevel,exceptDiff); } @Subscriber(@Param(Constant.EVENT_TOUCH_UP)) public void notifyTouchEnd(final UniversalEventBean eventBean) { mHandler.post(new Runnable() { @Override public void run() { long curTouchTime = eventBean.getTime(); LogUtil.w("yuawen", "上一次点击的时间:" + curTouchTime); if (hasClicked || curTouchTime < lastRecorderStartTime) { return; } hasClicked = true; lastCalculateT1 = curTouchTime - lastRecorderStartTime; LogUtil.w("yuawen", "筛选后上一次点击的时间:" + curTouchTime); LogUtil.w("yuawen", "t1 costs:" + lastCalculateT1); } }); } }