which I use as a record service in this repository.
9 |
10 | Visit this site to get a details explaining of Unity_Android_Screen_Recorder.
11 |
12 |
13 |
14 | **If you are using this library in one of your applications and would like to thank me:
**
15 |
16 | 
17 |
18 | or via PayPal:
19 |
20 |
21 |
22 | ## Build guide
23 |
24 | + Clone this project and open it using Android Studio.
25 | + Make module androidutils.
26 | + Wait for build process to complete then get your plugin in Android_Screen_Recorder/unityrecorder/build/generated/outputs/aar/unityrecorder-release.aar
27 | + Copy the unityrecorder-release.aar to Plugins/Android folder inside your project.
28 | + Add a AndroidManifest.xml in the same folder which have the content below:
29 |
30 | ```xml
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | ```
45 |
46 | Now your plugin is ready to use in any Unity project.
47 |
48 | ## Screen Record guide
49 |
50 | To record android screen you have to follow this instruction:
51 |
52 | ### 1.Set-up recorder
53 |
54 | Call this inside Start() function of your script.
55 |
56 | ```cs
57 |
58 | #if UNITY_ANDROID && !UNITY_EDITOR
59 | using (AndroidJavaClass unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
60 | {
61 | androidRecorder = unityClass.GetStatic("currentActivity");
62 | androidRecorder.Call("setUpSaveFolder","Tee");//custom your save folder to Movies/Tee, by defaut it will use Movies/AndroidUtils
63 | int width = (int)(Screen.width > SCREEN_WIDTH ? SCREEN_WIDTH : Screen.width);
64 | int height = Screen.width > SCREEN_WIDTH ? (int)(Screen.height * SCREEN_WIDTH / Screen.width) : Screen.height;
65 | int bitrate = (int)(1f * width * height / 100 * 240 * 7);
66 | int fps = 30;
67 | bool audioEnable=true;
68 | androidRecorder.Call("setupVideo", width, height,bitrate, fps, audioEnable, VideoEncoder.H264.ToString());//this line manual sets the video record setting. You can use the defaut setting by comment this code block
69 | }
70 | #endif
71 |
72 | ```
73 |
74 | ### 2. Start record
75 |
76 | Then call this to start record screen.
77 |
78 | ```cs
79 |
80 | #if UNITY_ANDROID && !UNITY_EDITOR
81 | if (!AndroidUtils.IsPermitted(AndroidPermission.RECORD_AUDIO))//RECORD_AUDIO is declared inside plugin manifest but we need to request it manualy
82 | {
83 | AndroidUtils.RequestPermission(AndroidPermission.RECORD_AUDIO);
84 | onAllowCallback = () =>
85 | {
86 | androidRecorder.Call("startRecording");
87 | };
88 | onDenyCallback = () => { ShowToast("Need RECORD_AUDIO permission to record voice");};
89 | onDenyAndNeverAskAgainCallback = () => { ShowToast("Need RECORD_AUDIO permission to record voice");};
90 | }
91 | else
92 | androidRecorder.Call("startRecording");
93 | #endif
94 | ```
95 |
96 | ### 3. Stop record
97 |
98 | Call
99 |
100 | ```cs
101 |
102 | androidRecorder.Call("stopRecording");
103 | ```
104 |
105 | ### 4. Handle callback
106 |
107 | Create a function name VideoRecorderCallback(string message) inside your project. It will receive callback from java side.
108 | List of messages from java:
109 |
110 | + init_record_error
111 | + start_record
112 | + stop_record
113 |
114 | ## Runtime Permissions guide
115 |
116 | Runtime permission allow you to request android permission at runtime(for android 6.0 and above). Example: When your game need to access location service, instead of request this permission at the first runtime now you can delay it until the moment your app actually need to use the permission.
117 | Note that. Your app can only request the permissions which have been declared in AndroidManifest.xml
118 |
119 | ### 1. You need to declare the permissions in AndroidManifest.xml. Then add this below meta-data to skip request permission dialog when you open app first time
120 |
121 | ```xml
122 |
123 |
124 |
125 | ```
126 |
127 | the final AndroidManifest.xml must look similar like this.
128 |
129 | ```xml
130 |
131 |
132 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | ```
146 |
147 | ### 2.To check if your app has a permision
148 |
149 | ```cs
150 |
151 | public static bool IsPermitted(AndroidPermission permission)
152 | {
153 | #if UNITY_ANDROID && !UNITY_EDITOR
154 | using (var androidUtils = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
155 | {
156 | return androidUtils.GetStatic("currentActivity").Call("hasPermission", GetPermissionStrr(permission));
157 | }
158 | #endif
159 | return true;
160 | }
161 |
162 | ```
163 |
164 | ### 3.To request a permission
165 |
166 | ```cs
167 |
168 | public static void RequestPermission(AndroidPermission permission, UnityAction onAllow = null, UnityAction onDeny = null, UnityAction onDenyAndNeverAskAgain = null)
169 | {
170 | #if UNITY_ANDROID && !UNITY_EDITOR
171 | onAllowCallback = onAllow;
172 | onDenyCallback = onDeny;
173 | onDenyAndNeverAskAgainCallback = onDenyAndNeverAskAgain;
174 | using (var androidUtils = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
175 | {
176 | androidUtils.GetStatic("currentActivity").Call("requestPermission", GetPermissionStrr(permission));
177 | }
178 | #endif
179 | }
180 | ```
181 |
182 | ### 4. Helper function to convert from enum to android permission string
183 |
184 | ```cs
185 |
186 | private static string GetPermissionStrr(AndroidPermission permission)
187 | {
188 | return "android.permission." + permission.ToString();
189 | }
190 | ```
191 |
192 | ### 5. List of android permissions
193 |
194 | ```cs
195 |
196 | public enum AndroidPermission
197 | {
198 | ACCESS_COARSE_LOCATION,
199 | ACCESS_FINE_LOCATION,
200 | ADD_VOICEMAIL,
201 | BODY_SENSORS,
202 | CALL_PHONE,
203 | CAMERA,
204 | GET_ACCOUNTS,
205 | PROCESS_OUTGOING_CALLS,
206 | READ_CALENDAR,
207 | READ_CALL_LOG,
208 | READ_CONTACTS,
209 | READ_EXTERNAL_STORAGE,
210 | READ_PHONE_STATE,
211 | READ_SMS,
212 | RECEIVE_MMS,
213 | RECEIVE_SMS,
214 | RECEIVE_WAP_PUSH,
215 | RECORD_AUDIO,
216 | SEND_SMS,
217 | USE_SIP,
218 | WRITE_CALENDAR,
219 | WRITE_CALL_LOG,
220 | WRITE_CONTACTS,
221 | WRITE_EXTERNAL_STORAGE
222 | }
223 |
224 | ```
225 |
226 | ### 6. Implement 3 functions below to receive callback when you request a permission
227 |
228 | ```cs
229 |
230 | private void OnAllow(){}
231 | private void OnDeny(){}
232 | private void OnDenyAndNeverAskAgain(){}
233 |
234 | ```
235 |
--------------------------------------------------------------------------------
/androidutils/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/androidutils/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | }
4 |
5 | android {
6 | compileSdkVersion 30
7 | buildToolsVersion "30.0.3"
8 |
9 | defaultConfig {
10 | minSdkVersion 21
11 | targetSdkVersion 30
12 | versionCode 1
13 | versionName "1.0"
14 |
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | consumerProguardFiles "consumer-rules.pro"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_1_8
27 | targetCompatibility JavaVersion.VERSION_1_8
28 | }
29 | }
30 |
31 | dependencies {
32 | implementation 'androidx.appcompat:appcompat:1.2.0'
33 | provided files('classes.jar')
34 | }
--------------------------------------------------------------------------------
/androidutils/classes.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanh-nguyen-kim/Android_Screen_Recorder_Plugin/9c7dc05cd93c4965f153caa1bbbd68105aa6cebd/androidutils/classes.jar
--------------------------------------------------------------------------------
/androidutils/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanh-nguyen-kim/Android_Screen_Recorder_Plugin/9c7dc05cd93c4965f153caa1bbbd68105aa6cebd/androidutils/consumer-rules.pro
--------------------------------------------------------------------------------
/androidutils/libs/hbrecorder-release.aar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanh-nguyen-kim/Android_Screen_Recorder_Plugin/9c7dc05cd93c4965f153caa1bbbd68105aa6cebd/androidutils/libs/hbrecorder-release.aar
--------------------------------------------------------------------------------
/androidutils/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/androidutils/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/androidutils/src/main/java/com/hbisoft/hbrecorder/Constants.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | public class Constants {
4 | public final static String MAX_FILE_SIZE_KEY = "maxFileSize";
5 | public final static String ERROR_REASON_KEY = "errorReason";
6 | public final static String ERROR_KEY = "error";
7 | public final static String ON_COMPLETE_KEY = "onComplete";
8 | public final static String ON_START_KEY = "onStart";
9 | public final static String ON_COMPLETE = "Uri was passed";
10 | public final static int SETTINGS_ERROR = 38;
11 | public final static int MAX_FILE_SIZE_REACHED_ERROR = 48;
12 | public final static int GENERAL_ERROR = 100;
13 | public final static int ON_START = 111;
14 | public final static int NO_SPECIFIED_MAX_SIZE = 0;
15 | }
16 |
--------------------------------------------------------------------------------
/androidutils/src/main/java/com/hbisoft/hbrecorder/Countdown.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import java.util.Timer;
4 | import java.util.TimerTask;
5 |
6 | public abstract class Countdown extends Timer {
7 | private long totalTime, interval, delay;
8 | private TimerTask task;
9 | private long startTime = -1;
10 | private boolean restart = false, wasCancelled = false, wasStarted = false;
11 |
12 | public Countdown(long totalTime, long interval) {
13 | this(totalTime, interval, 0);
14 | }
15 |
16 | public Countdown(long totalTime, long interval, long delay) {
17 | super("PreciseCountdown", true);
18 | this.delay = delay;
19 | this.interval = interval;
20 | this.totalTime = totalTime;
21 | this.task = getTask(totalTime);
22 | }
23 |
24 | public void start() {
25 | wasStarted = true;
26 | this.scheduleAtFixedRate(task, delay, interval);
27 | }
28 |
29 | public void stop() {
30 | onStopCalled();
31 | this.wasCancelled = true;
32 | this.task.cancel();
33 | dispose();
34 | }
35 |
36 | // Call this when there's no further use for this timer
37 | public void dispose(){
38 | cancel();
39 | purge();
40 | }
41 |
42 | private TimerTask getTask(final long totalTime) {
43 | return new TimerTask() {
44 |
45 | @Override
46 | public void run() {
47 | long timeLeft;
48 | if (startTime < 0 || restart) {
49 | startTime = scheduledExecutionTime();
50 | timeLeft = totalTime;
51 | restart = false;
52 | } else {
53 | timeLeft = totalTime - (scheduledExecutionTime() - startTime);
54 |
55 | if (timeLeft <= 0) {
56 | this.cancel();
57 | startTime = -1;
58 | onFinished();
59 | return;
60 | }
61 | }
62 |
63 | onTick(timeLeft);
64 | }
65 | };
66 | }
67 |
68 | public abstract void onTick(long timeLeft);
69 | public abstract void onFinished();
70 | public abstract void onStopCalled();
71 | }
72 |
--------------------------------------------------------------------------------
/androidutils/src/main/java/com/hbisoft/hbrecorder/FileObserver.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import java.io.File;
4 | import java.util.ArrayList;
5 | import java.util.List;
6 | import java.util.Stack;
7 |
8 | import android.app.Activity;
9 |
10 |
11 | class FileObserver extends android.os.FileObserver {
12 |
13 | private List mObservers;
14 | private final String mPath;
15 | private final int mMask;
16 | private final Activity activity;
17 | private final MyListener ml;
18 |
19 | FileObserver(String path, Activity activity, MyListener ml) {
20 | super(path, ALL_EVENTS);
21 | mPath = path;
22 | mMask = ALL_EVENTS;
23 |
24 | this.activity = activity;
25 | this.ml = ml;
26 |
27 | }
28 |
29 |
30 | @Override
31 | public void startWatching() {
32 | if (mObservers != null) return;
33 |
34 | mObservers = new ArrayList<>();
35 | Stack stack = new Stack<>();
36 | stack.push(mPath);
37 |
38 | while (!stack.isEmpty()) {
39 | String parent = stack.pop();
40 | mObservers.add(new SingleFileObserver(parent, mMask));
41 | File path = new File(parent);
42 | File[] files = path.listFiles();
43 | if (null == files) continue;
44 |
45 | for (File f : files) {
46 | if (f.isDirectory() && !f.getName().equals(".") && !f.getName().equals("..")) {
47 | stack.push(f.getPath());
48 | }
49 | }
50 | }
51 |
52 | for (SingleFileObserver sfo : mObservers) {
53 | sfo.startWatching();
54 | }
55 | }
56 |
57 | @Override
58 | public void stopWatching() {
59 | if (mObservers == null) return;
60 |
61 | for (SingleFileObserver sfo : mObservers) {
62 | sfo.stopWatching();
63 | }
64 | mObservers.clear();
65 | mObservers = null;
66 | }
67 |
68 | @Override
69 | public void onEvent(int event, final String path) {
70 | if (event == android.os.FileObserver.CLOSE_WRITE) {
71 | activity.runOnUiThread(new Runnable() {
72 | public void run() {
73 | ml.callback();
74 | }
75 | });
76 | }
77 | }
78 |
79 | class SingleFileObserver extends android.os.FileObserver {
80 | final String mPath;
81 |
82 |
83 | SingleFileObserver(String path, int mask) {
84 | super(path, mask);
85 | mPath = path;
86 | }
87 |
88 | @Override
89 | public void onEvent(int event, String path) {
90 | String newPath = mPath + "/" + path;
91 | FileObserver.this.onEvent(event, newPath);
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/androidutils/src/main/java/com/hbisoft/hbrecorder/HBRecorder.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import android.app.Activity;
4 | import android.app.ActivityManager;
5 | import android.content.Context;
6 | import android.content.Intent;
7 | import android.content.res.Resources;
8 | import android.graphics.Bitmap;
9 | import android.graphics.BitmapFactory;
10 | import android.net.Uri;
11 | import android.os.Build;
12 | import android.os.Bundle;
13 | import android.os.CountDownTimer;
14 | import android.os.Environment;
15 |
16 | import androidx.annotation.DrawableRes;
17 | import androidx.annotation.RequiresApi;
18 |
19 | import android.os.Handler;
20 | import android.os.ResultReceiver;
21 | import android.util.DisplayMetrics;
22 | import android.util.Log;
23 |
24 |
25 | import java.io.ByteArrayOutputStream;
26 | import java.io.File;
27 |
28 | import static com.hbisoft.hbrecorder.Constants.ERROR_KEY;
29 | import static com.hbisoft.hbrecorder.Constants.ERROR_REASON_KEY;
30 | import static com.hbisoft.hbrecorder.Constants.GENERAL_ERROR;
31 | import static com.hbisoft.hbrecorder.Constants.MAX_FILE_SIZE_KEY;
32 | import static com.hbisoft.hbrecorder.Constants.NO_SPECIFIED_MAX_SIZE;
33 | import static com.hbisoft.hbrecorder.Constants.ON_COMPLETE_KEY;
34 | import static com.hbisoft.hbrecorder.Constants.ON_START_KEY;
35 |
36 | /**
37 | * Created by HBiSoft on 13 Aug 2019
38 | * Copyright (c) 2019 . All rights reserved.
39 | */
40 |
41 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
42 | public class HBRecorder implements MyListener {
43 | private int mScreenWidth;
44 | private int mScreenHeight;
45 | private int mScreenDensity;
46 | private final Context context;
47 | private int resultCode;
48 | private boolean isAudioEnabled = true;
49 | private boolean isVideoHDEnabled = true;
50 | private Activity activity;
51 | private String outputPath;
52 | private String fileName;
53 | private String notificationTitle;
54 | private String notificationDescription;
55 | private String notificationButtonText;
56 | private int audioBitrate = 0;
57 | private int audioSamplingRate = 0;
58 | private FileObserver observer;
59 | private final HBRecorderListener hbRecorderListener;
60 | private byte[] byteArray;
61 | private String audioSource = "MIC";
62 | private String videoEncoder = "DEFAULT";
63 | private boolean enableCustomSettings = false;
64 | private int videoFrameRate = 30;
65 | private int videoBitrate = 40000000;
66 | private String outputFormat = "DEFAULT";
67 | private int orientation;
68 | private long maxFileSize = NO_SPECIFIED_MAX_SIZE; // Default no max size
69 | boolean wasOnErrorCalled = false;
70 | Intent service;
71 | boolean isPaused = false;
72 | boolean isMaxDurationSet = false;
73 | int maxDuration = 0;
74 |
75 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
76 | public HBRecorder(Context context, HBRecorderListener listener) {
77 | this.context = context.getApplicationContext();
78 | this.hbRecorderListener = listener;
79 | setScreenDensity();
80 | }
81 |
82 | public void setOrientationHint(int orientationInDegrees){
83 | orientation = orientationInDegrees;
84 | }
85 |
86 | /*Set output path*/
87 | public void setOutputPath(String path) {
88 | outputPath = path;
89 | }
90 |
91 | Uri mUri;
92 | boolean mWasUriSet = false;
93 | @RequiresApi(api = Build.VERSION_CODES.Q)
94 | public void setOutputUri(Uri uri){
95 | mWasUriSet = true;
96 | mUri = uri;
97 | }
98 |
99 | /*Set max duration in seconds */
100 | public void setMaxDuration(int seconds){
101 | isMaxDurationSet = true;
102 | maxDuration = seconds * 1000;
103 | }
104 |
105 | /*Set max file size in kb*/
106 | public void setMaxFileSize(long fileSize) {
107 | maxFileSize = fileSize;
108 | }
109 |
110 | public boolean wasUriSet(){
111 | return mWasUriSet;
112 | }
113 |
114 | /*Set file name*/
115 | public void setFileName(String fileName) {
116 | this.fileName = fileName;
117 | }
118 |
119 | /*Set audio bitrate*/
120 | public void setAudioBitrate(int audioBitrate) {
121 | this.audioBitrate = audioBitrate;
122 |
123 | }
124 |
125 | /*Set audio sample rate*/
126 | public void setAudioSamplingRate(int audioSamplingRate) {
127 | this.audioSamplingRate = audioSamplingRate;
128 | }
129 |
130 | /*Enable/Disable audio*/
131 | public void isAudioEnabled(boolean bool) {
132 | this.isAudioEnabled = bool;
133 | }
134 |
135 | /*Set Audio Source*/
136 | //MUST BE ONE OF THE FOLLOWING - https://developer.android.com/reference/android/media/MediaRecorder.AudioSource.html
137 | public void setAudioSource(String source){
138 | audioSource = source;
139 |
140 | }
141 |
142 | /*Enable/Disable HD recording*/
143 | public void recordHDVideo(boolean bool) {
144 | this.isVideoHDEnabled = bool;
145 | }
146 |
147 | /*Set Video Encoder*/
148 | //MUST BE ONE OF THE FOLLOWING - https://developer.android.com/reference/android/media/MediaRecorder.VideoEncoder.html
149 | public void setVideoEncoder(String encoder){
150 | videoEncoder = encoder;
151 |
152 | }
153 |
154 | //Enable Custom Settings
155 | public void enableCustomSettings(){
156 | enableCustomSettings = true;
157 |
158 | }
159 |
160 | //Set Video Frame Rate
161 | public void setVideoFrameRate(int fps){
162 | videoFrameRate = fps;
163 | }
164 |
165 | //Set Video BitRate
166 | public void setVideoBitrate(int bitrate){
167 | videoBitrate = bitrate;
168 | }
169 |
170 | //Set Output Format
171 | //MUST BE ONE OF THE FOLLOWING - https://developer.android.com/reference/android/media/MediaRecorder.OutputFormat.html
172 | public void setOutputFormat(String format){
173 | outputFormat = format;
174 | }
175 |
176 | // Set screen densityDpi
177 | private void setScreenDensity() {
178 | DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
179 | mScreenDensity = metrics.densityDpi;
180 | }
181 |
182 | //Get default width
183 | public int getDefaultWidth(){
184 | HBRecorderCodecInfo hbRecorderCodecInfo = new HBRecorderCodecInfo();
185 | hbRecorderCodecInfo.setContext(context);
186 | return hbRecorderCodecInfo.getMaxSupportedWidth();
187 | }
188 |
189 | //Get default height
190 | public int getDefaultHeight(){
191 | HBRecorderCodecInfo hbRecorderCodecInfo = new HBRecorderCodecInfo();
192 | hbRecorderCodecInfo.setContext(context);
193 | return hbRecorderCodecInfo.getMaxSupportedHeight();
194 | }
195 |
196 | //Set Custom Dimensions (NOTE - YOUR DEVICE MIGHT NOT SUPPORT THE SIZE YOU PASS IT)
197 | public void setScreenDimensions(int heightInPX, int widthInPX){
198 | mScreenHeight = heightInPX;
199 | mScreenWidth = widthInPX;
200 | }
201 |
202 | /*Get file path including file name and extension*/
203 | public String getFilePath() {
204 | return ScreenRecordService.getFilePath();
205 | }
206 |
207 | /*Get file name and extension*/
208 | public String getFileName() {
209 | return ScreenRecordService.getFileName();
210 | }
211 |
212 | /*Start screen recording*/
213 | public void startScreenRecording(Intent data, int resultCode, Activity activity) {
214 | this.resultCode = resultCode;
215 | this.activity = activity;
216 | startService(data);
217 | }
218 |
219 | /*Stop screen recording*/
220 | public void stopScreenRecording() {
221 | Intent service = new Intent(context, ScreenRecordService.class);
222 | context.stopService(service);
223 | }
224 |
225 | /*Pause screen recording*/
226 | @RequiresApi(api = Build.VERSION_CODES.N)
227 | public void pauseScreenRecording(){
228 | if (service != null){
229 | isPaused = true;
230 | service.setAction("pause");
231 | context.startService(service);
232 | }
233 | }
234 |
235 | /*Pause screen recording*/
236 | @RequiresApi(api = Build.VERSION_CODES.N)
237 | public void resumeScreenRecording(){
238 | if (service != null){
239 | isPaused = false;
240 | service.setAction("resume");
241 | context.startService(service);
242 | }
243 | }
244 |
245 | /*Check if video is paused*/
246 | public boolean isRecordingPaused(){
247 | return isPaused;
248 | }
249 |
250 | /*Check if recording is in progress*/
251 | public boolean isBusyRecording() {
252 | ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
253 | if (manager != null) {
254 | for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
255 | if (ScreenRecordService.class.getName().equals(service.service.getClassName())) {
256 | return true;
257 | }
258 | }
259 | }
260 | return false;
261 | }
262 |
263 | /*Change notification icon*/
264 | public void setNotificationSmallIcon(@DrawableRes int drawable) {
265 | Bitmap icon = BitmapFactory.decodeResource(context.getResources(), drawable);
266 | ByteArrayOutputStream stream = new ByteArrayOutputStream();
267 | icon.compress(Bitmap.CompressFormat.PNG, 100, stream);
268 | byteArray = stream.toByteArray();
269 | }
270 |
271 | /*Change notification icon*/
272 | public void setNotificationSmallIcon(byte[] bytes) {
273 | byteArray = bytes;
274 | }
275 |
276 | /*Set notification title*/
277 | public void setNotificationTitle(String Title) {
278 | notificationTitle = Title;
279 | }
280 |
281 | /*Set notification description*/
282 | public void setNotificationDescription(String Description) {
283 | notificationDescription = Description;
284 | }
285 |
286 | public void setNotificationButtonText(String string){
287 | notificationButtonText = string;
288 | }
289 |
290 | /*Start recording service*/
291 | private void startService(Intent data) {
292 | try {
293 | if (!mWasUriSet) {
294 | if (outputPath != null) {
295 | File file = new File(outputPath);
296 | String parent = file.getParent();
297 | observer = new FileObserver(parent, activity, HBRecorder.this);
298 | } else {
299 | observer = new FileObserver(String.valueOf(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)), activity, HBRecorder.this);
300 | }
301 | observer.startWatching();
302 | }
303 |
304 | service = new Intent(context, ScreenRecordService.class);
305 | if (mWasUriSet) {
306 | service.putExtra("mUri", mUri.toString());
307 | }
308 | service.putExtra("code", resultCode);
309 | service.putExtra("data", data);
310 | service.putExtra("audio", isAudioEnabled);
311 | service.putExtra("width", mScreenWidth);
312 | service.putExtra("height", mScreenHeight);
313 | service.putExtra("density", mScreenDensity);
314 | service.putExtra("quality", isVideoHDEnabled);
315 | service.putExtra("path", outputPath);
316 | service.putExtra("fileName", fileName);
317 | service.putExtra("orientation", orientation);
318 | service.putExtra("audioBitrate", audioBitrate);
319 | service.putExtra("audioSamplingRate", audioSamplingRate);
320 | service.putExtra("notificationSmallBitmap", byteArray);
321 | service.putExtra("notificationTitle", notificationTitle);
322 | service.putExtra("notificationDescription", notificationDescription);
323 | service.putExtra("notificationButtonText", notificationButtonText);
324 | service.putExtra("enableCustomSettings", enableCustomSettings);
325 | service.putExtra("audioSource",audioSource);
326 | service.putExtra("videoEncoder", videoEncoder);
327 |
328 | service.putExtra("videoFrameRate", videoFrameRate);
329 | service.putExtra("videoBitrate", videoBitrate);
330 | service.putExtra("outputFormat", outputFormat);
331 | service.putExtra(ScreenRecordService.BUNDLED_LISTENER, new ResultReceiver(new Handler()) {
332 | @Override
333 | protected void onReceiveResult(int resultCode, Bundle resultData) {
334 | super.onReceiveResult(resultCode, resultData);
335 | if (resultCode == Activity.RESULT_OK) {
336 | String errorListener = resultData.getString(ERROR_REASON_KEY);
337 | String onComplete = resultData.getString(ON_COMPLETE_KEY);
338 | int onStartCode = resultData.getInt(ON_START_KEY);
339 | int errorCode = resultData.getInt(ERROR_KEY);
340 | if (errorListener != null) {
341 | //Stop countdown if it was set
342 | stopCountDown();
343 | if (!mWasUriSet) {
344 | observer.stopWatching();
345 | }
346 | wasOnErrorCalled = true;
347 | if ( errorCode > 0 ) {
348 | hbRecorderListener.HBRecorderOnError(errorCode, errorListener);
349 | } else {
350 | hbRecorderListener.HBRecorderOnError(GENERAL_ERROR, errorListener);
351 | }
352 | try {
353 | Intent mService = new Intent(context, ScreenRecordService.class);
354 | context.stopService(mService);
355 | }catch (Exception e){
356 | // Can be ignored
357 | }
358 |
359 | }else if (onComplete != null){
360 | //Stop countdown if it was set
361 | stopCountDown();
362 | //OnComplete for when Uri was passed
363 | if (mWasUriSet && !wasOnErrorCalled) {
364 | hbRecorderListener.HBRecorderOnComplete();
365 | }
366 | wasOnErrorCalled = false;
367 | }else if (onStartCode != 0){
368 | hbRecorderListener.HBRecorderOnStart();
369 | //Check if max duration was set and start count down
370 | if (isMaxDurationSet){
371 | startCountdown();
372 | }
373 | }
374 | }
375 | }
376 | });
377 | // Max file size
378 | service.putExtra(MAX_FILE_SIZE_KEY, maxFileSize);
379 | context.startService(service);
380 | }catch (Exception e){
381 | hbRecorderListener.HBRecorderOnError(0, Log.getStackTraceString(e));
382 | }
383 |
384 | }
385 |
386 | /*CountdownTimer for when max duration is set*/
387 | Countdown countDown = null;
388 | private void startCountdown() {
389 | countDown = new Countdown(maxDuration, 1000, 0) {
390 | @Override
391 | public void onTick(long timeLeft) {
392 | // Could add a callback to provide the time to the user
393 | // Will add if users request this
394 | }
395 |
396 | @Override
397 | public void onFinished() {
398 | onTick(0);
399 | // Since the timer is running on a different thread
400 | // UI chances should be called from the UI Thread
401 | activity.runOnUiThread(new Runnable() {
402 | @Override
403 | public void run() {
404 | try {
405 | stopScreenRecording();
406 | observer.stopWatching();
407 | hbRecorderListener.HBRecorderOnComplete();
408 | } catch (Exception e){
409 | e.printStackTrace();
410 | }
411 | }
412 | });
413 | }
414 |
415 | @Override
416 | public void onStopCalled() {
417 | // Currently unused, but might be helpful in the future
418 | }
419 | };
420 | countDown.start();
421 | }
422 |
423 | private void stopCountDown(){
424 | if (countDown != null) {
425 | countDown.stop();
426 | }
427 | }
428 |
429 | /*Complete callback method*/
430 | @Override
431 | public void callback() {
432 | observer.stopWatching();
433 | hbRecorderListener.HBRecorderOnComplete();
434 | }
435 | }
436 |
--------------------------------------------------------------------------------
/androidutils/src/main/java/com/hbisoft/hbrecorder/HBRecorderCodecInfo.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import android.content.Context;
4 | import android.content.res.Configuration;
5 | import android.media.CamcorderProfile;
6 | import android.media.MediaCodecInfo;
7 | import android.media.MediaCodecList;
8 | import android.os.Build;
9 | import android.util.DisplayMetrics;
10 | import android.util.Range;
11 | import android.view.WindowManager;
12 |
13 | import androidx.annotation.RequiresApi;
14 |
15 | import java.util.ArrayList;
16 | import java.util.HashMap;
17 |
18 | import static android.content.Context.WINDOW_SERVICE;
19 | import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
20 | import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
21 |
22 | /**
23 | * Created by HBiSoft on 13 Aug 2019
24 | * Copyright (c) 2019 . All rights reserved.
25 | */
26 |
27 | public class HBRecorderCodecInfo {
28 |
29 | int getMaxSupportedWidth(){
30 | RecordingInfo recordingInfo = getRecordingInfo();
31 | return recordingInfo.width;
32 | }
33 |
34 | int getMaxSupportedHeight(){
35 | RecordingInfo recordingInfo = getRecordingInfo();
36 | return recordingInfo.height;
37 | }
38 |
39 | private RecordingInfo getRecordingInfo() {
40 | DisplayMetrics displayMetrics = new DisplayMetrics();
41 | WindowManager wm = (WindowManager) context.getSystemService(WINDOW_SERVICE);
42 | wm.getDefaultDisplay().getRealMetrics(displayMetrics);
43 | int displayWidth = displayMetrics.widthPixels;
44 | int displayHeight = displayMetrics.heightPixels;
45 | int displayDensity = displayMetrics.densityDpi;
46 |
47 | Configuration configuration = context.getResources().getConfiguration();
48 | boolean isLandscape = configuration.orientation == ORIENTATION_LANDSCAPE;
49 |
50 | CamcorderProfile camcorderProfile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH);
51 | int cameraWidth = camcorderProfile != null ? camcorderProfile.videoFrameWidth : -1;
52 | int cameraHeight = camcorderProfile != null ? camcorderProfile.videoFrameHeight : -1;
53 | int cameraFrameRate = camcorderProfile != null ? camcorderProfile.videoFrameRate : 30;
54 |
55 |
56 | return calculateRecordingInfo(displayWidth, displayHeight, displayDensity, isLandscape,
57 | cameraWidth, cameraHeight, cameraFrameRate, 100);
58 | }
59 |
60 | private Context context;
61 |
62 | public void setContext(Context c) {
63 | context = c;
64 | }
65 |
66 | static final class RecordingInfo {
67 | final int width;
68 | final int height;
69 | final int frameRate;
70 | final int density;
71 |
72 | RecordingInfo(int width, int height, int frameRate, int density) {
73 | this.width = width;
74 | this.height = height;
75 | this.frameRate = frameRate;
76 | this.density = density;
77 | }
78 | }
79 |
80 | static RecordingInfo calculateRecordingInfo(int displayWidth, int displayHeight, int displayDensity, boolean isLandscapeDevice, int cameraWidth, int cameraHeight, int cameraFrameRate, int sizePercentage) {
81 | // Scale the display size before any maximum size calculations.
82 | displayWidth = displayWidth * sizePercentage / 100;
83 | displayHeight = displayHeight * sizePercentage / 100;
84 |
85 | if (cameraWidth == -1 && cameraHeight == -1) {
86 | // No cameras. Fall back to the display size.
87 | return new RecordingInfo(displayWidth, displayHeight, cameraFrameRate, displayDensity);
88 | }
89 |
90 | int frameWidth = isLandscapeDevice ? cameraWidth : cameraHeight;
91 | int frameHeight = isLandscapeDevice ? cameraHeight : cameraWidth;
92 | if (frameWidth >= displayWidth && frameHeight >= displayHeight) {
93 | // Frame can hold the entire display. Use exact values.
94 | return new RecordingInfo(displayWidth, displayHeight, cameraFrameRate, displayDensity);
95 | }
96 |
97 | // Calculate new width or height to preserve aspect ratio.
98 | if (isLandscapeDevice) {
99 | frameWidth = displayWidth * frameHeight / displayHeight;
100 | } else {
101 | frameHeight = displayHeight * frameWidth / displayWidth;
102 | }
103 | return new RecordingInfo(frameWidth, frameHeight, cameraFrameRate, displayDensity);
104 | }
105 |
106 |
107 | // ALL PUBLIC METHODS
108 | // This is for testing purposes only
109 |
110 | // Select the default video encoder
111 | // This will only return the default video encoder, it does not mean that the encoder supports a particular set of parameters
112 | public final MediaCodecInfo selectVideoCodec(final String mimeType) {
113 | MediaCodecInfo result = null;
114 | // get the list of available codecs
115 | final int numCodecs = MediaCodecList.getCodecCount();
116 | for (int i = 0; i < numCodecs; i++) {
117 | final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
118 | if (!codecInfo.isEncoder()) { // skipp decoder
119 | continue;
120 | }
121 | String[] types = codecInfo.getSupportedTypes();
122 | for (String type : types) {
123 | if (type.equalsIgnoreCase(mimeType)) {
124 | return codecInfo;
125 | }
126 | }
127 | }
128 | return result;
129 | }
130 |
131 | private String selectCodecByMime(String mimeType) {
132 | int numCodecs = MediaCodecList.getCodecCount();
133 | for (int i = 0; i < numCodecs; i++) {
134 | MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
135 | if (!codecInfo.isEncoder()) {
136 | continue;
137 | }
138 | String[] types = codecInfo.getSupportedTypes();
139 | for (String type : types) {
140 | if (type.equalsIgnoreCase(mimeType)) {
141 | return codecInfo.getName();
142 | }
143 | }
144 | }
145 | return "Mime not supported";
146 | }
147 |
148 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
149 | private MediaCodecInfo selectDefaultCodec() {
150 | MediaCodecInfo result = null;
151 | // get the list of available codecs
152 | final int numCodecs = MediaCodecList.getCodecCount();
153 | for (int i = 0; i < numCodecs; i++) {
154 | final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
155 | if (!codecInfo.isEncoder()) { // skipp decoder
156 | continue;
157 | }
158 | final String[] types = codecInfo.getSupportedTypes();
159 | for (int j = 0; j < types.length; j++) {
160 | if (types[j].contains("video")){
161 | MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(types[j]);
162 | boolean formatSup = codecCapabilities.isFormatSupported(codecCapabilities.getDefaultFormat());
163 | if (formatSup) {
164 | return codecInfo;
165 | }
166 | }
167 | }
168 | }
169 | return result;
170 | }
171 |
172 | // Get the default video encoder name
173 | // The default one will be returned first
174 | public String getDefaultVideoEncoderName(String mimeType){
175 | String defaultEncoder = "";
176 | try {
177 | defaultEncoder = selectCodecByMime(mimeType);
178 | }catch (Exception e){
179 | e.printStackTrace();
180 | }
181 | return defaultEncoder;
182 | }
183 |
184 | // Get the default video format
185 | // The default one will be returned first
186 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
187 | public String getDefaultVideoFormat(){
188 | String supported = "";
189 | try {
190 | final MediaCodecInfo codecInfo = selectDefaultCodec();
191 | if (codecInfo != null) {
192 | String[] types = codecInfo.getSupportedTypes();
193 | for (String type : types) {
194 | if (type.contains("video")) {
195 | MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(type);
196 | String result = codecCapabilities.getDefaultFormat().toString();
197 | return returnTypeFromMime(result.substring(result.indexOf("=") + 1, result.indexOf(",")));
198 | }
199 | }
200 | }else {
201 | supported = "null";
202 | }
203 | }catch (Exception e){
204 | e.printStackTrace();
205 | }
206 | return supported;
207 | }
208 |
209 | private String returnTypeFromMime(String mimeType){
210 | switch (mimeType) {
211 | case "video/MP2T":
212 | return "MPEG_2_TS";
213 | case "video/mp4v-es":
214 | return "MPEG_4";
215 | case "video/mp4v":
216 | return "MPEG_4";
217 | case "video/mp4":
218 | return "MPEG_4";
219 | case "video/avc":
220 | return "MPEG_4";
221 | case "video/3gpp":
222 | return "THREE_GPP";
223 | case "video/webm":
224 | return "WEBM";
225 | case "video/x-vnd.on2.vp8":
226 | return "WEBM";
227 | }
228 | return "";
229 | }
230 |
231 | // Example usage - isSizeAndFramerateSupported(hbrecorder.getWidth(), hbrecorder.getHeight(), 30, "video/mp4", ORIENTATION_PORTRAIT);
232 | // int width - The width of the view to be recorder
233 | // int height - The height of the view to be recorder
234 | // String mimeType - for ex. video/mp4
235 | // int orientation - ORIENTATION_PORTRAIT or ORIENTATION_LANDSCAPE
236 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
237 | public boolean isSizeAndFramerateSupported(int width, int height, int fps, String mimeType, int orientation){
238 | boolean supported = false;
239 | try {
240 | final MediaCodecInfo codecInfo = selectVideoCodec(mimeType);
241 | String[] types = codecInfo.getSupportedTypes();
242 | for (String type : types) {
243 | if (type.contains("video")) {
244 | MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(type);
245 | MediaCodecInfo.VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
246 |
247 | // Flip around the width and height in ORIENTATION_PORTRAIT because android's default orientation is ORIENTATION_LANDSCAPE
248 | if (ORIENTATION_PORTRAIT == orientation) {
249 | supported = videoCapabilities.areSizeAndRateSupported(height, width, fps);
250 | }else {
251 | supported = videoCapabilities.areSizeAndRateSupported(width, height, fps);
252 | }
253 | }
254 | }
255 | }catch (Exception e){
256 | e.printStackTrace();
257 | }
258 | return supported;
259 | }
260 |
261 | public boolean isMimeTypeSupported(String mimeType){
262 | try {
263 | final MediaCodecInfo codecInfo = selectVideoCodec(mimeType);
264 | String[] types = codecInfo.getSupportedTypes();
265 | for (String type : types) {
266 | if (type.contains("video")) {
267 | //ignore
268 | }
269 | }
270 | }catch (Exception e){
271 | return false;
272 | }
273 | return true;
274 | }
275 |
276 | // Check if a particular size is supported
277 | // Provide width and height in Portrait mode, for example. isSizeSupported(1080, 1920, "video/mp4");
278 | // We do this because android's default orientation is landscape, so we have to flip around the width and height
279 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
280 | public boolean isSizeSupported(int width, int height, String mimeType){
281 | boolean supported = false;
282 | try {
283 | final MediaCodecInfo codecInfo = selectVideoCodec(mimeType);
284 | String[] types = codecInfo.getSupportedTypes();
285 | for (String type : types) {
286 | if (type.contains("video")) {
287 | MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(type);
288 | MediaCodecInfo.VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
289 | supported = videoCapabilities.isSizeSupported(height, width);
290 | }
291 | }
292 | }catch (Exception e){
293 | e.printStackTrace();
294 | }
295 | return supported;
296 | }
297 |
298 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
299 | public double getMaxSupportedFrameRate(int width, int height, String mimeType){
300 | double maxFPS = 0;
301 | try {
302 | final MediaCodecInfo codecInfo = selectVideoCodec(mimeType);
303 | String[] types = codecInfo.getSupportedTypes();
304 | for (String type : types) {
305 | if (type.contains("video")) {
306 | MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(type);
307 | MediaCodecInfo.VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
308 | Range bit = videoCapabilities.getSupportedFrameRatesFor(height, width);
309 | maxFPS = bit.getUpper();
310 | }
311 | }
312 | }catch (Exception e){
313 | e.printStackTrace();
314 | }
315 | return maxFPS;
316 | }
317 |
318 | // Get the max supported bitrate for a particular mime type
319 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
320 | public int getMaxSupportedBitrate(String mimeType){
321 | int bitrate = 0;
322 | try {
323 | final MediaCodecInfo codecInfo = selectVideoCodec(mimeType);
324 | String[] types = codecInfo.getSupportedTypes();
325 | for (String type : types) {
326 | if (type.contains("video")) {
327 | MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType(type);
328 | MediaCodecInfo.VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities();
329 | Range bit = videoCapabilities.getBitrateRange();
330 |
331 | bitrate = bit.getUpper();
332 |
333 | }
334 | }
335 | }catch (Exception e){
336 | e.printStackTrace();
337 | }
338 | return bitrate;
339 | }
340 |
341 | // Get supported video formats
342 | ArrayList supportedVideoFormats = new ArrayList<>();
343 | public ArrayList getSupportedVideoFormats(){
344 | String[] allFormats = {"video/MP2T", "video/mp4v-es", "video/m4v", "video/mp4", "video/avc", "video/3gpp", "video/webm", "video/x-vnd.on2.vp8"};
345 |
346 | for (String allFormat : allFormats) {
347 | checkSupportedVideoFormats(allFormat);
348 | }
349 | return supportedVideoFormats;
350 | }
351 | private void checkSupportedVideoFormats(String mimeType){
352 | // get the list of available codecs
353 | final int numCodecs = MediaCodecList.getCodecCount();
354 | LOOP:
355 | for (int i = 0; i < numCodecs; i++) {
356 | final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
357 | if (!codecInfo.isEncoder()) { // skipp decoder
358 | continue;
359 | }
360 | final String[] types = codecInfo.getSupportedTypes();
361 | for (int j = 0; j < types.length; j++) {
362 | if (types[j].contains("video")){
363 | switch (mimeType) {
364 | case "video/MP2T":
365 | supportedVideoFormats.add("MPEG_2_TS");
366 | break LOOP;
367 | case "video/mp4v-es":
368 | if(!supportedVideoFormats.contains("MPEG_4")) {
369 | supportedVideoFormats.add("MPEG_4");
370 | }
371 | break LOOP;
372 | case "video/mp4v":
373 | if(!supportedVideoFormats.contains("MPEG_4")) {
374 | supportedVideoFormats.add("MPEG_4");
375 | }
376 | break LOOP;
377 | case "video/mp4":
378 | if(!supportedVideoFormats.contains("MPEG_4")) {
379 | supportedVideoFormats.add("MPEG_4");
380 | }
381 | break LOOP;
382 | case "video/avc":
383 | if(!supportedVideoFormats.contains("MPEG_4")) {
384 | supportedVideoFormats.add("MPEG_4");
385 | }
386 | break LOOP;
387 | case "video/3gpp":
388 | supportedVideoFormats.add("THREE_GPP");
389 | break LOOP;
390 |
391 | case "video/webm":
392 | if(!supportedVideoFormats.contains("WEBM")) {
393 | supportedVideoFormats.add("WEBM");
394 | }
395 | break LOOP;
396 | case "video/video/x-vnd.on2.vp8":
397 | if(!supportedVideoFormats.contains("WEBM")) {
398 | supportedVideoFormats.add("WEBM");
399 | }
400 | break LOOP;
401 |
402 | }
403 | }
404 | }
405 | }
406 | }
407 |
408 | // Get supported audio formats
409 | ArrayList supportedAudioFormats = new ArrayList<>();
410 | public ArrayList getSupportedAudioFormats(){
411 | String[] allFormats = {"audio/amr_nb", "audio/amr_wb", "audio/x-hx-aac-adts", "audio/ogg"};
412 |
413 | for (String allFormat : allFormats) {
414 | checkSupportedAudioFormats(allFormat);
415 | }
416 | return supportedAudioFormats;
417 | }
418 | private void checkSupportedAudioFormats(String mimeType){
419 | // get the list of available codecs
420 | final int numCodecs = MediaCodecList.getCodecCount();
421 | LOOP:
422 | for (int i = 0; i < numCodecs; i++) {
423 | final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
424 | if (!codecInfo.isEncoder()) { // skipp decoder
425 | continue;
426 | }
427 | final String[] types = codecInfo.getSupportedTypes();
428 | label:
429 | for (int j = 0; j < types.length; j++) {
430 | if (types[j].contains("audio")){
431 | switch (mimeType) {
432 | case "audio/amr_nb":
433 | supportedAudioFormats.add("AMR_NB");
434 | break LOOP;
435 | case "audio/amr_wb":
436 | supportedAudioFormats.add("AMR_WB");
437 | break LOOP;
438 | case "audio/x-hx-aac-adts":
439 | supportedAudioFormats.add("AAC_ADTS");
440 | break LOOP;
441 | case "audio/ogg":
442 | supportedAudioFormats.add("OGG");
443 | break LOOP;
444 | }
445 | }
446 | }
447 | }
448 | }
449 |
450 | // Get supported video mime types
451 | HashMap mVideoMap= new HashMap<>();
452 | public HashMap getSupportedVideoMimeTypes(){
453 | checkIfSupportedVideoMimeTypes();
454 | return mVideoMap;
455 | }
456 | private void checkIfSupportedVideoMimeTypes(){
457 | // get the list of available codecs
458 | final int numCodecs = MediaCodecList.getCodecCount();
459 | for (int i = 0; i < numCodecs; i++) {
460 | final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
461 | if (!codecInfo.isEncoder()) { // skipp decoder
462 | continue;
463 | }
464 | final String[] types = codecInfo.getSupportedTypes();
465 | for (String type : types) {
466 | if (type.contains("video")) {
467 | mVideoMap.put(codecInfo.getName(), type);
468 | }
469 | }
470 | }
471 | }
472 |
473 | // Get supported audio mime types
474 | HashMap mAudioMap= new HashMap<>();
475 | public HashMap getSupportedAudioMimeTypes(){
476 | checkIfSupportedAudioMimeTypes();
477 | return mAudioMap;
478 | }
479 | private void checkIfSupportedAudioMimeTypes(){
480 | // get the list of available codecs
481 | final int numCodecs = MediaCodecList.getCodecCount();
482 | for (int i = 0; i < numCodecs; i++) {
483 | final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
484 | if (!codecInfo.isEncoder()) { // skipp decoder
485 | continue;
486 | }
487 | final String[] types = codecInfo.getSupportedTypes();
488 | for (String type : types) {
489 | if (type.contains("audio")) {
490 | mAudioMap.put(codecInfo.getName(), type);
491 | }
492 | }
493 | }
494 | }
495 |
496 | }
497 |
--------------------------------------------------------------------------------
/androidutils/src/main/java/com/hbisoft/hbrecorder/HBRecorderListener.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | public interface HBRecorderListener {
4 | void HBRecorderOnStart();
5 | void HBRecorderOnComplete();
6 | void HBRecorderOnError(int errorCode, String reason);
7 | }
8 |
--------------------------------------------------------------------------------
/androidutils/src/main/java/com/hbisoft/hbrecorder/MyListener.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | interface MyListener {
4 | void callback();
5 |
6 | }
7 |
--------------------------------------------------------------------------------
/androidutils/src/main/java/com/hbisoft/hbrecorder/NotificationReceiver.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import android.content.BroadcastReceiver;
4 | import android.content.Context;
5 | import android.content.Intent;
6 |
7 | public class NotificationReceiver extends BroadcastReceiver {
8 |
9 | @Override
10 | public void onReceive(Context context, Intent intent) {
11 | Intent service = new Intent(context, ScreenRecordService.class);
12 | context.stopService(service);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/androidutils/src/main/java/com/hbisoft/hbrecorder/ScreenRecordService.java:
--------------------------------------------------------------------------------
1 | package com.hbisoft.hbrecorder;
2 |
3 | import android.app.Activity;
4 | import android.app.Notification;
5 | import android.app.NotificationChannel;
6 | import android.app.NotificationManager;
7 | import android.app.PendingIntent;
8 | import android.app.Service;
9 | import android.content.ContentResolver;
10 | import android.content.Context;
11 | import android.content.Intent;
12 | import android.graphics.Bitmap;
13 | import android.graphics.BitmapFactory;
14 | import android.graphics.Color;
15 | import android.graphics.drawable.Icon;
16 | import android.hardware.display.DisplayManager;
17 | import android.hardware.display.VirtualDisplay;
18 | import android.media.MediaRecorder;
19 | import android.media.projection.MediaProjection;
20 | import android.media.projection.MediaProjectionManager;
21 | import android.net.Uri;
22 | import android.os.Build;
23 | import android.os.Bundle;
24 | import android.os.Environment;
25 | import android.os.IBinder;
26 |
27 | import androidx.annotation.RequiresApi;
28 |
29 | import android.os.ResultReceiver;
30 | import android.util.Log;
31 |
32 | import java.io.FileDescriptor;
33 | import java.sql.Date;
34 | import java.text.SimpleDateFormat;
35 | import java.util.Locale;
36 | import java.util.Objects;
37 |
38 | import static com.hbisoft.hbrecorder.Constants.ERROR_KEY;
39 | import static com.hbisoft.hbrecorder.Constants.ERROR_REASON_KEY;
40 | import static com.hbisoft.hbrecorder.Constants.MAX_FILE_SIZE_REACHED_ERROR;
41 | import static com.hbisoft.hbrecorder.Constants.MAX_FILE_SIZE_KEY;
42 | import static com.hbisoft.hbrecorder.Constants.NO_SPECIFIED_MAX_SIZE;
43 | import static com.hbisoft.hbrecorder.Constants.ON_COMPLETE;
44 | import static com.hbisoft.hbrecorder.Constants.ON_COMPLETE_KEY;
45 | import static com.hbisoft.hbrecorder.Constants.ON_START;
46 | import static com.hbisoft.hbrecorder.Constants.ON_START_KEY;
47 | import static com.hbisoft.hbrecorder.Constants.SETTINGS_ERROR;
48 |
49 | /**
50 | * Created by HBiSoft on 13 Aug 2019
51 | * Copyright (c) 2019 . All rights reserved.
52 | */
53 |
54 | public class ScreenRecordService extends Service {
55 |
56 | private static final String TAG = "ScreenRecordService";
57 | private long maxFileSize = NO_SPECIFIED_MAX_SIZE;
58 | private boolean hasMaxFileBeenReached = false;
59 | private int mScreenWidth;
60 | private int mScreenHeight;
61 | private int mScreenDensity;
62 | private int mResultCode;
63 | private Intent mResultData;
64 | private boolean isVideoHD;
65 | private boolean isAudioEnabled;
66 | private String path;
67 |
68 | private MediaProjection mMediaProjection;
69 | private MediaRecorder mMediaRecorder;
70 | private VirtualDisplay mVirtualDisplay;
71 | private String name;
72 | private int audioBitrate;
73 | private int audioSamplingRate;
74 | private static String filePath;
75 | private static String fileName;
76 | private int audioSourceAsInt;
77 | private int videoEncoderAsInt;
78 | private boolean isCustomSettingsEnabled;
79 | private int videoFrameRate;
80 | private int videoBitrate;
81 | private int outputFormatAsInt;
82 | private int orientationHint;
83 |
84 | public final static String BUNDLED_LISTENER = "listener";
85 | private Uri returnedUri = null;
86 | private Intent mIntent;
87 |
88 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
89 | @Override
90 | public int onStartCommand(final Intent intent, int flags, int startId) {
91 | String pauseResumeAction = intent.getAction();
92 | //Pause Recording
93 | if (pauseResumeAction != null && pauseResumeAction.equals("pause")){
94 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
95 | pauseRecording();
96 | }
97 | }
98 | //Resume Recording
99 | else if (pauseResumeAction != null && pauseResumeAction.equals("resume")){
100 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
101 | resumeRecording();
102 | }
103 | }
104 | //Start Recording
105 | else {
106 | //Get intent extras
107 | hasMaxFileBeenReached = false;
108 | mIntent = intent;
109 | maxFileSize = intent.getLongExtra(MAX_FILE_SIZE_KEY, NO_SPECIFIED_MAX_SIZE);
110 | byte[] notificationSmallIcon = intent.getByteArrayExtra("notificationSmallBitmap");
111 | String notificationTitle = intent.getStringExtra("notificationTitle");
112 | String notificationDescription = intent.getStringExtra("notificationDescription");
113 | String notificationButtonText = intent.getStringExtra("notificationButtonText");
114 | orientationHint = intent.getIntExtra("orientation", 400);
115 | mResultCode = intent.getIntExtra("code", -1);
116 | mResultData = intent.getParcelableExtra("data");
117 | mScreenWidth = intent.getIntExtra("width", 0);
118 | mScreenHeight = intent.getIntExtra("height", 0);
119 |
120 | if (intent.getStringExtra("mUri") != null) {
121 | returnedUri = Uri.parse(intent.getStringExtra("mUri"));
122 | }
123 |
124 | if (mScreenHeight == 0 || mScreenWidth == 0) {
125 | HBRecorderCodecInfo hbRecorderCodecInfo = new HBRecorderCodecInfo();
126 | hbRecorderCodecInfo.setContext(this);
127 | mScreenHeight = hbRecorderCodecInfo.getMaxSupportedHeight();
128 | mScreenWidth = hbRecorderCodecInfo.getMaxSupportedWidth();
129 | }
130 |
131 | mScreenDensity = intent.getIntExtra("density", 1);
132 | isVideoHD = intent.getBooleanExtra("quality", true);
133 | isAudioEnabled = intent.getBooleanExtra("audio", true);
134 | path = intent.getStringExtra("path");
135 | name = intent.getStringExtra("fileName");
136 | String audioSource = intent.getStringExtra("audioSource");
137 | String videoEncoder = intent.getStringExtra("videoEncoder");
138 | videoFrameRate = intent.getIntExtra("videoFrameRate", 30);
139 | videoBitrate = intent.getIntExtra("videoBitrate", 40000000);
140 |
141 | if (audioSource != null) {
142 | setAudioSourceAsInt(audioSource);
143 | }
144 | if (videoEncoder != null) {
145 | setvideoEncoderAsInt(videoEncoder);
146 | }
147 |
148 | filePath = name;
149 | audioBitrate = intent.getIntExtra("audioBitrate", 128000);
150 | audioSamplingRate = intent.getIntExtra("audioSamplingRate", 44100);
151 | String outputFormat = intent.getStringExtra("outputFormat");
152 | if (outputFormat != null) {
153 | setOutputFormatAsInt(outputFormat);
154 | }
155 |
156 | isCustomSettingsEnabled = intent.getBooleanExtra("enableCustomSettings", false);
157 |
158 | //Set notification notification button text if developer did not
159 | if (notificationButtonText == null) {
160 | notificationButtonText = "STOP RECORDING";
161 | }
162 | //Set notification bitrate if developer did not
163 | if (audioBitrate == 0) {
164 | audioBitrate = 128000;
165 | }
166 | //Set notification sampling rate if developer did not
167 | if (audioSamplingRate == 0) {
168 | audioSamplingRate = 44100;
169 | }
170 | //Set notification title if developer did not
171 | if (notificationTitle == null || notificationTitle.equals("")) {
172 | notificationTitle = getString(R.string.stop_recording_notification_title);
173 | }
174 | //Set notification description if developer did not
175 | if (notificationDescription == null || notificationDescription.equals("")) {
176 | notificationDescription = getString(R.string.stop_recording_notification_message);
177 | }
178 |
179 | //Notification
180 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
181 | String channelId = "001";
182 | String channelName = "RecordChannel";
183 | NotificationChannel channel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_NONE);
184 | channel.setLightColor(Color.BLUE);
185 | channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
186 | NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
187 | if (manager != null) {
188 | manager.createNotificationChannel(channel);
189 | Notification notification;
190 |
191 | Intent myIntent = new Intent(this, NotificationReceiver.class);
192 |
193 | PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, myIntent, 0);
194 |
195 | Notification.Action action = new Notification.Action.Builder(
196 | Icon.createWithResource(this, android.R.drawable.presence_video_online),
197 | notificationButtonText,
198 | pendingIntent).build();
199 |
200 | if (notificationSmallIcon != null) {
201 | Bitmap bmp = BitmapFactory.decodeByteArray(notificationSmallIcon, 0, notificationSmallIcon.length);
202 | //Modify notification badge
203 | notification = new Notification.Builder(getApplicationContext(), channelId).setOngoing(true).setSmallIcon(Icon.createWithBitmap(bmp)).setContentTitle(notificationTitle).setContentText(notificationDescription).addAction(action).build();
204 |
205 | } else {
206 | //Modify notification badge
207 | notification = new Notification.Builder(getApplicationContext(), channelId).setOngoing(true).setSmallIcon(R.drawable.icon).setContentTitle(notificationTitle).setContentText(notificationDescription).addAction(action).build();
208 | }
209 | startForeground(101, notification);
210 | }
211 | } else {
212 | startForeground(101, new Notification());
213 | }
214 |
215 |
216 | if (returnedUri == null) {
217 | if (path == null) {
218 | path = String.valueOf(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES));
219 | }
220 | }
221 |
222 | //Init MediaRecorder
223 | try {
224 | initRecorder();
225 | } catch (Exception e) {
226 | ResultReceiver receiver = intent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
227 | Bundle bundle = new Bundle();
228 | bundle.putString(ERROR_REASON_KEY, Log.getStackTraceString(e));
229 | if (receiver != null) {
230 | receiver.send(Activity.RESULT_OK, bundle);
231 | }
232 | }
233 |
234 | //Init MediaProjection
235 | try {
236 | initMediaProjection();
237 | } catch (Exception e) {
238 | ResultReceiver receiver = intent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
239 | Bundle bundle = new Bundle();
240 | bundle.putString(ERROR_REASON_KEY, Log.getStackTraceString(e));
241 | if (receiver != null) {
242 | receiver.send(Activity.RESULT_OK, bundle);
243 | }
244 | }
245 |
246 | //Init VirtualDisplay
247 | try {
248 | initVirtualDisplay();
249 | } catch (Exception e) {
250 | ResultReceiver receiver = intent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
251 | Bundle bundle = new Bundle();
252 | bundle.putString(ERROR_REASON_KEY, Log.getStackTraceString(e));
253 | if (receiver != null) {
254 | receiver.send(Activity.RESULT_OK, bundle);
255 | }
256 | }
257 |
258 | mMediaRecorder.setOnErrorListener(new MediaRecorder.OnErrorListener() {
259 | @Override
260 | public void onError(MediaRecorder mediaRecorder, int what, int extra) {
261 | if ( what == 268435556 && hasMaxFileBeenReached) {
262 | // Benign error b/c recording is too short and has no frames. See SO: https://stackoverflow.com/questions/40616466/mediarecorder-stop-failed-1007
263 | return;
264 | }
265 | ResultReceiver receiver = intent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
266 | Bundle bundle = new Bundle();
267 | bundle.putInt(ERROR_KEY, SETTINGS_ERROR);
268 | bundle.putString(ERROR_REASON_KEY, String.valueOf(what));
269 | if (receiver != null) {
270 | receiver.send(Activity.RESULT_OK, bundle);
271 | }
272 | }
273 | });
274 |
275 | mMediaRecorder.setOnInfoListener(new MediaRecorder.OnInfoListener() {
276 | @Override
277 | public void onInfo(MediaRecorder mr, int what, int extra) {
278 | if (what == MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
279 | hasMaxFileBeenReached = true;
280 | Log.i(TAG,String.format(Locale.US,"onInfoListen what : %d | extra %d", what, extra));
281 | ResultReceiver receiver = intent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
282 | Bundle bundle = new Bundle();
283 | bundle.putInt(ERROR_KEY, MAX_FILE_SIZE_REACHED_ERROR);
284 | bundle.putString(ERROR_REASON_KEY, getString(R.string.max_file_reached));
285 | if (receiver != null) {
286 | receiver.send(Activity.RESULT_OK, bundle);
287 | }
288 | }
289 | }
290 | });
291 |
292 | //Start Recording
293 | try {
294 | mMediaRecorder.start();
295 | ResultReceiver receiver = intent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
296 | Bundle bundle = new Bundle();
297 | bundle.putInt(ON_START_KEY, ON_START);
298 | if (receiver != null) {
299 | receiver.send(Activity.RESULT_OK, bundle);
300 | }
301 | } catch (Exception e) {
302 | // From the tests I've done, this can happen if another application is using the mic or if an unsupported video encoder was selected
303 | ResultReceiver receiver = intent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
304 | Bundle bundle = new Bundle();
305 | bundle.putInt(ERROR_KEY, SETTINGS_ERROR);
306 | bundle.putString(ERROR_REASON_KEY, Log.getStackTraceString(e));
307 | if (receiver != null) {
308 | receiver.send(Activity.RESULT_OK, bundle);
309 | }
310 | }
311 | }
312 |
313 |
314 | return Service.START_STICKY;
315 | }
316 |
317 | //Pause Recording
318 | @RequiresApi(api = Build.VERSION_CODES.N)
319 | private void pauseRecording(){
320 | mMediaRecorder.pause();
321 | }
322 |
323 | //Resume Recording
324 | @RequiresApi(api = Build.VERSION_CODES.N)
325 | private void resumeRecording(){
326 | mMediaRecorder.resume();
327 | }
328 |
329 | //Set output format as int based on what developer has provided
330 | //It is important to provide one of the following and nothing else.
331 | private void setOutputFormatAsInt(String outputFormat) {
332 | switch (outputFormat) {
333 | case "DEFAULT":
334 | outputFormatAsInt = 0;
335 | break;
336 | case "THREE_GPP":
337 | outputFormatAsInt = 1;
338 | break;
339 | case "AMR_NB":
340 | outputFormatAsInt = 3;
341 | break;
342 | case "AMR_WB":
343 | outputFormatAsInt = 4;
344 | break;
345 | case "AAC_ADTS":
346 | outputFormatAsInt = 6;
347 | break;
348 | case "MPEG_2_TS":
349 | outputFormatAsInt = 8;
350 | break;
351 | case "WEBM":
352 | outputFormatAsInt = 9;
353 | break;
354 | case "OGG":
355 | outputFormatAsInt = 11;
356 | break;
357 | case "MPEG_4":
358 | default:
359 | outputFormatAsInt = 2;
360 | }
361 | }
362 |
363 | //Set video encoder as int based on what developer has provided
364 | //It is important to provide one of the following and nothing else.
365 | private void setvideoEncoderAsInt(String encoder) {
366 | switch (encoder) {
367 | case "DEFAULT":
368 | videoEncoderAsInt = 0;
369 | break;
370 | case "H263":
371 | videoEncoderAsInt = 1;
372 | break;
373 | case "H264":
374 | videoEncoderAsInt = 2;
375 | break;
376 | case "MPEG_4_SP":
377 | videoEncoderAsInt = 3;
378 | break;
379 | case "VP8":
380 | videoEncoderAsInt = 4;
381 | break;
382 | case "HEVC":
383 | videoEncoderAsInt = 5;
384 | break;
385 | }
386 | }
387 |
388 | //Set audio source as int based on what developer has provided
389 | //It is important to provide one of the following and nothing else.
390 | private void setAudioSourceAsInt(String audioSource) {
391 | switch (audioSource) {
392 | case "DEFAULT":
393 | audioSourceAsInt = 0;
394 | break;
395 | case "MIC":
396 | audioSourceAsInt = 1;
397 | break;
398 | case "VOICE_UPLINK":
399 | audioSourceAsInt = 2;
400 | break;
401 | case "VOICE_DOWNLINK":
402 | audioSourceAsInt = 3;
403 | break;
404 | case "VOICE_CALL":
405 | audioSourceAsInt = 4;
406 | break;
407 | case "CAMCODER":
408 | audioSourceAsInt = 5;
409 | break;
410 | case "VOICE_RECOGNITION":
411 | audioSourceAsInt = 6;
412 | break;
413 | case "VOICE_COMMUNICATION":
414 | audioSourceAsInt = 7;
415 | break;
416 | case "REMOTE_SUBMIX":
417 | audioSourceAsInt = 8;
418 | break;
419 | case "UNPROCESSED":
420 | audioSourceAsInt = 9;
421 | break;
422 | case "VOICE_PERFORMANCE":
423 | audioSourceAsInt = 10;
424 | break;
425 | }
426 | }
427 |
428 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
429 | private void initMediaProjection() {
430 | mMediaProjection = ((MediaProjectionManager) Objects.requireNonNull(getSystemService(Context.MEDIA_PROJECTION_SERVICE))).getMediaProjection(mResultCode, mResultData);
431 | }
432 |
433 | //Return the output file path as string
434 | public static String getFilePath() {
435 | return filePath;
436 | }
437 |
438 | //Return the name of the output file
439 | public static String getFileName() {
440 | return fileName;
441 | }
442 |
443 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
444 | private void initRecorder() throws Exception {
445 | SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault());
446 | Date curDate = new Date(System.currentTimeMillis());
447 | String curTime = formatter.format(curDate).replace(" ", "");
448 | String videoQuality = "HD";
449 | if (!isVideoHD) {
450 | videoQuality = "SD";
451 | }
452 | if (name == null) {
453 | name = videoQuality + curTime;
454 | }
455 |
456 | filePath = path + "/" + name + ".mp4";
457 |
458 | fileName = name + ".mp4";
459 |
460 | mMediaRecorder = new MediaRecorder();
461 |
462 |
463 | if (isAudioEnabled) {
464 | mMediaRecorder.setAudioSource(audioSourceAsInt);
465 | }
466 | mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
467 | mMediaRecorder.setOutputFormat(outputFormatAsInt);
468 |
469 | if (orientationHint != 400){
470 | mMediaRecorder.setOrientationHint(orientationHint);
471 | }
472 |
473 | if (isAudioEnabled) {
474 | mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
475 | mMediaRecorder.setAudioEncodingBitRate(audioBitrate);
476 | mMediaRecorder.setAudioSamplingRate(audioSamplingRate);
477 | }
478 |
479 | mMediaRecorder.setVideoEncoder(videoEncoderAsInt);
480 |
481 |
482 | if (returnedUri != null) {
483 | try {
484 | ContentResolver contentResolver = getContentResolver();
485 | FileDescriptor inputPFD = Objects.requireNonNull(contentResolver.openFileDescriptor(returnedUri, "rw")).getFileDescriptor();
486 | mMediaRecorder.setOutputFile(inputPFD);
487 | } catch (Exception e) {
488 | ResultReceiver receiver = mIntent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
489 | Bundle bundle = new Bundle();
490 | bundle.putString(ERROR_REASON_KEY, Log.getStackTraceString(e));
491 | if (receiver != null) {
492 | receiver.send(Activity.RESULT_OK, bundle);
493 | }
494 | }
495 | }else{
496 | mMediaRecorder.setOutputFile(filePath);
497 | }
498 | mMediaRecorder.setVideoSize(mScreenWidth, mScreenHeight);
499 |
500 | if (!isCustomSettingsEnabled) {
501 | if (!isVideoHD) {
502 | mMediaRecorder.setVideoEncodingBitRate(12000000);
503 | mMediaRecorder.setVideoFrameRate(30);
504 | } else {
505 | mMediaRecorder.setVideoEncodingBitRate(5 * mScreenWidth * mScreenHeight);
506 | mMediaRecorder.setVideoFrameRate(60); //after setVideoSource(), setOutFormat()
507 | }
508 | } else {
509 | mMediaRecorder.setVideoEncodingBitRate(videoBitrate);
510 | mMediaRecorder.setVideoFrameRate(videoFrameRate);
511 | }
512 |
513 | // Catch approaching file limit
514 | if ( maxFileSize > NO_SPECIFIED_MAX_SIZE) {
515 | mMediaRecorder.setMaxFileSize(maxFileSize); // in bytes
516 | }
517 |
518 | mMediaRecorder.prepare();
519 |
520 | }
521 |
522 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
523 | private void initVirtualDisplay() {
524 | mVirtualDisplay = mMediaProjection.createVirtualDisplay(TAG, mScreenWidth, mScreenHeight, mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mMediaRecorder.getSurface(), null, null);
525 | }
526 |
527 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
528 | @Override
529 | public void onDestroy() {
530 | super.onDestroy();
531 | resetAll();
532 | callOnComplete();
533 |
534 | }
535 |
536 | private void callOnComplete() {
537 | if ( mIntent != null ) {
538 | ResultReceiver receiver = mIntent.getParcelableExtra(ScreenRecordService.BUNDLED_LISTENER);
539 | Bundle bundle = new Bundle();
540 | bundle.putString(ON_COMPLETE_KEY, ON_COMPLETE);
541 | if (receiver != null) {
542 | receiver.send(Activity.RESULT_OK, bundle);
543 | }
544 | }
545 | }
546 |
547 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
548 | private void resetAll() {
549 | stopForeground(true);
550 | if (mVirtualDisplay != null) {
551 | mVirtualDisplay.release();
552 | mVirtualDisplay = null;
553 | }
554 | if (mMediaRecorder != null) {
555 | mMediaRecorder.setOnErrorListener(null);
556 | mMediaRecorder.reset();
557 | }
558 | if (mMediaProjection != null) {
559 | mMediaProjection.stop();
560 | mMediaProjection = null;
561 | }
562 | }
563 |
564 | @Override
565 | public IBinder onBind(Intent intent) {
566 | return null;
567 | }
568 | }
569 |
--------------------------------------------------------------------------------
/androidutils/src/main/java/com/setik/androidutils/AndroidUtils.java:
--------------------------------------------------------------------------------
1 | package com.setik.androidutils;
2 |
3 |
4 | import android.content.ContentResolver;
5 | import android.content.ContentValues;
6 | import android.content.Context;
7 | import android.content.Intent;
8 | import android.content.pm.PackageManager;
9 | import android.graphics.Bitmap;
10 | import android.graphics.BitmapFactory;
11 | import android.media.MediaScannerConnection;
12 | import android.media.projection.MediaProjectionManager;
13 | import android.net.Uri;
14 | import android.os.Build;
15 | import android.os.Bundle;
16 | import android.os.Environment;
17 | import android.provider.MediaStore;
18 | import android.util.Log;
19 | import android.widget.Toast;
20 |
21 | import androidx.annotation.DrawableRes;
22 | import androidx.annotation.RequiresApi;
23 |
24 | import com.hbisoft.hbrecorder.*;
25 | import com.unity3d.player.UnityPlayer;
26 | import com.unity3d.player.UnityPlayerActivity;
27 |
28 | import java.io.ByteArrayOutputStream;
29 | import java.io.File;
30 | import java.text.SimpleDateFormat;
31 | import java.util.Date;
32 | import java.util.Locale;
33 |
34 | public class AndroidUtils extends UnityPlayerActivity implements HBRecorderListener {
35 | private static final int SCREEN_RECORD_REQUEST_CODE = 777;
36 | private static final int PERMISSION_REQ_ID_RECORD_AUDIO = 22;
37 | private static final int PERMISSION_REQ_ID_WRITE_EXTERNAL_STORAGE = PERMISSION_REQ_ID_RECORD_AUDIO + 1;
38 | private String mGameObject = "AndroidUtils", saveFolder = "AndroidUtils";
39 | HBRecorder hbRecorder;
40 | ContentValues contentValues;
41 | Uri mUri;
42 | ContentResolver resolver;
43 | boolean customSetting = false;
44 |
45 | @Override
46 | public void HBRecorderOnStart() {
47 | Log.e("HBRecorder", "HBRecorderOnStart called");
48 | UnityPlayer.UnitySendMessage(this.mGameObject, "VideoRecorderCallback", "start_record");
49 | }
50 |
51 | @Override
52 | public void HBRecorderOnComplete() {
53 | showLongToast("Saved Successfully");
54 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
55 | //Update gallery depending on SDK Level
56 | if (hbRecorder.wasUriSet()) {
57 | updateGalleryUri();
58 | } else {
59 | refreshGalleryFile();
60 | }
61 | }
62 | UnityPlayer.UnitySendMessage(this.mGameObject, "VideoRecorderCallback", "stop_record");
63 | }
64 |
65 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
66 | private void refreshGalleryFile() {
67 | MediaScannerConnection.scanFile(this,
68 | new String[]{hbRecorder.getFilePath()}, null,
69 | new MediaScannerConnection.OnScanCompletedListener() {
70 | public void onScanCompleted(String path, Uri uri) {
71 | Log.i("ExternalStorage", "Scanned " + path + ":");
72 | Log.i("ExternalStorage", "-> uri=" + uri);
73 | }
74 | });
75 | }
76 |
77 | private void updateGalleryUri() {
78 | contentValues.clear();
79 | contentValues.put(MediaStore.Video.Media.IS_PENDING, 0);
80 | getContentResolver().update(mUri, contentValues, null, null);
81 | }
82 |
83 | @Override
84 | public void HBRecorderOnError(int errorCode, String reason) {
85 | // Error 38 happens when
86 | // - the selected video encoder is not supported
87 | // - the output format is not supported
88 | // - if another app is using the microphone
89 | //It is best to use device default
90 | if (errorCode == 38) {
91 | showLongToast("Some settings are not supported by your device");
92 | } else {
93 | showLongToast("HBRecorderOnError - See Log");
94 | Log.e("HBRecorderOnError", reason);
95 | }
96 | UnityPlayer.UnitySendMessage(this.mGameObject, "VideoRecorderCallback", "init_record_error");
97 | }
98 |
99 | protected void onCreate(Bundle savedInstanceState) {
100 | super.onCreate(savedInstanceState);
101 | hbRecorder = new HBRecorder(this, this);
102 | }
103 |
104 | public void setUpSaveFolder(String folderName) {
105 | this.saveFolder = folderName;
106 | }
107 |
108 | public void setupVideo(int width, int height, int bitRate, int fps, boolean audioEnabled) {
109 | hbRecorder.enableCustomSettings();
110 | hbRecorder.setScreenDimensions(height, width);
111 | hbRecorder.setVideoFrameRate(fps);
112 | hbRecorder.setVideoBitrate(bitRate);
113 | hbRecorder.setAudioBitrate(128000);
114 | hbRecorder.setAudioSamplingRate(44100);
115 | hbRecorder.isAudioEnabled(audioEnabled);
116 | customSetting = true;
117 | }
118 | public void setupVideo(int width, int height, int bitRate, int fps, boolean audioEnabled,String encoder) {
119 | hbRecorder.enableCustomSettings();
120 | hbRecorder.setScreenDimensions(height, width);
121 | hbRecorder.setVideoFrameRate(fps);
122 | hbRecorder.setVideoBitrate(bitRate);
123 | hbRecorder.setVideoEncoder(encoder);
124 | hbRecorder.setAudioBitrate(128000);
125 | hbRecorder.setAudioSamplingRate(44100);
126 | hbRecorder.isAudioEnabled(audioEnabled);
127 | customSetting = true;
128 | }
129 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
130 | private void startRecording() {
131 | if (!customSetting)
132 | quickSettings();
133 | MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
134 | Intent permissionIntent = mediaProjectionManager != null ? mediaProjectionManager.createScreenCaptureIntent() : null;
135 | startActivityForResult(permissionIntent, SCREEN_RECORD_REQUEST_CODE);
136 | }
137 |
138 | public void stopRecording() {
139 | hbRecorder.stopScreenRecording();
140 | UnityPlayer.UnitySendMessage(this.mGameObject, "VideoRecorderCallback", "stop_record");
141 | }
142 |
143 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
144 | private void quickSettings() {
145 | hbRecorder.setAudioBitrate(128000);
146 | hbRecorder.setAudioSamplingRate(44100);
147 | hbRecorder.recordHDVideo(false);
148 | hbRecorder.isAudioEnabled(true);
149 | //Customise Notification
150 | hbRecorder.setNotificationSmallIcon(drawable2ByteArray(R.drawable.icon));
151 | hbRecorder.setNotificationTitle("Recording your screen");
152 | hbRecorder.setNotificationDescription("Drag down to stop the recording");
153 | }
154 |
155 | @Override
156 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
157 | super.onActivityResult(requestCode, resultCode, data);
158 | if (requestCode == SCREEN_RECORD_REQUEST_CODE) {
159 | if (resultCode == RESULT_OK) {
160 | setOutputPath();
161 | //Start screen recording
162 | UnityPlayer.UnitySendMessage(mGameObject, "VideoRecorderCallback", "start_record");
163 | hbRecorder.startScreenRecording(data, resultCode, this);
164 | }
165 | }
166 | }
167 |
168 | //For Android 10> we will pass a Uri to HBRecorder
169 | //This is not necessary - You can still use getExternalStoragePublicDirectory
170 | //But then you will have to add android:requestLegacyExternalStorage="true" in your Manifest
171 | //IT IS IMPORTANT TO SET THE FILE NAME THE SAME AS THE NAME YOU USE FOR TITLE AND DISPLAY_NAME
172 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
173 | private void setOutputPath() {
174 | String filename = generateFileName();
175 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
176 | resolver = getContentResolver();
177 | contentValues = new ContentValues();
178 | contentValues.put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/" + saveFolder);
179 | contentValues.put(MediaStore.Video.Media.TITLE, filename);
180 | contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, filename);
181 | contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
182 | mUri = resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues);
183 | //FILE NAME SHOULD BE THE SAME
184 | hbRecorder.setFileName(filename);
185 | hbRecorder.setOutputUri(mUri);
186 | } else {
187 | createFolder();
188 | hbRecorder.setOutputPath(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES) + saveFolder);
189 | }
190 | }
191 |
192 | //Create Folder
193 | //Only call this on Android 9 and lower (getExternalStoragePublicDirectory is deprecated)
194 | //This can still be used on Android 10> but you will have to add android:requestLegacyExternalStorage="true" in your Manifest
195 | private void createFolder() {
196 | File f1 = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), saveFolder);
197 | if (!f1.exists()) {
198 | if (f1.mkdirs()) {
199 | Log.i("Folder ", "created");
200 | }
201 | }
202 | }
203 |
204 | //Generate a timestamp to be used as a file name
205 | private String generateFileName() {
206 | SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault());
207 | Date curDate = new Date(System.currentTimeMillis());
208 | return formatter.format(curDate).replace(" ", "");
209 | }
210 |
211 | private void showLongToast(final String msg) {
212 | Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show();
213 | }
214 |
215 | private byte[] drawable2ByteArray(@DrawableRes int drawableId) {
216 | Bitmap icon = BitmapFactory.decodeResource(getResources(), drawableId);
217 | ByteArrayOutputStream stream = new ByteArrayOutputStream();
218 | icon.compress(Bitmap.CompressFormat.PNG, 100, stream);
219 | return stream.toByteArray();
220 | }
221 |
222 | @RequiresApi(api = Build.VERSION_CODES.M)
223 | public void requestPermission(String permissionStr) {
224 | if ((!hasPermission(permissionStr)) && (android.os.Build.VERSION.SDK_INT >= 23)) {
225 | UnityPlayer.currentActivity.requestPermissions(new String[]{permissionStr}, 0);
226 | }
227 | }
228 |
229 | public boolean hasPermission(String permissionStr) {
230 | if (android.os.Build.VERSION.SDK_INT < 23)
231 | return true;
232 | Context context = UnityPlayer.currentActivity.getApplicationContext();
233 | return context.checkCallingOrSelfPermission(permissionStr) == PackageManager.PERMISSION_GRANTED;
234 | }
235 |
236 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
237 | super.onRequestPermissionsResult(requestCode, permissions, grantResults);
238 | switch (requestCode) {
239 | case 0:
240 | if (grantResults[0] == 0) {
241 | UnityPlayer.UnitySendMessage(mGameObject, "OnAllow", "");
242 | } else if (android.os.Build.VERSION.SDK_INT >= 23) {
243 | if (shouldShowRequestPermissionRationale(permissions[0])) {
244 | UnityPlayer.UnitySendMessage(mGameObject, "OnDeny", "");
245 | } else {
246 | UnityPlayer.UnitySendMessage(mGameObject, "OnDenyAndNeverAskAgain", "");
247 | }
248 | }
249 | break;
250 | }
251 | }
252 | }
253 |
--------------------------------------------------------------------------------
/androidutils/src/main/res/drawable/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanh-nguyen-kim/Android_Screen_Recorder_Plugin/9c7dc05cd93c4965f153caa1bbbd68105aa6cebd/androidutils/src/main/res/drawable/icon.png
--------------------------------------------------------------------------------
/androidutils/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | HBRecorder
3 | File size max has been reached.
4 | Drag down to stop the recording
5 | Recording your screen
6 |
7 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | }
4 |
5 | android {
6 | compileSdkVersion 29
7 | buildToolsVersion "30.0.5"
8 |
9 | defaultConfig {
10 | applicationId "com.setik.androidutils"
11 | minSdkVersion 21
12 | targetSdkVersion 29
13 | versionCode 1
14 | versionName "1.0"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_1_8
27 | targetCompatibility JavaVersion.VERSION_1_8
28 | }
29 | }
30 |
31 | dependencies {
32 | implementation 'androidx.appcompat:appcompat:1.2.0'
33 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
7 |
8 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | repositories {
4 | google()
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath "com.android.tools.build:gradle:4.1.0"
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | google()
18 | jcenter()
19 | maven { url 'https://jitpack.io' }
20 | }
21 | }
22 |
23 | task clean(type: Delete) {
24 | delete rootProject.buildDir
25 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thanh-nguyen-kim/Android_Screen_Recorder_Plugin/9c7dc05cd93c4965f153caa1bbbd68105aa6cebd/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Nov 04 20:46:31 ICT 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':androidutils'
2 | include ':app'
3 | rootProject.name = "androidutils"
--------------------------------------------------------------------------------