├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── libs │ └── soundwave-release.aar ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── soundwave │ │ └── jwkj │ │ └── com │ │ └── soundwavedemo │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── jwkj │ │ │ └── soundwavedemo │ │ │ ├── MainActivity.java │ │ │ ├── StartActivity.java │ │ │ └── runtimepermissions │ │ │ ├── Permissions.java │ │ │ ├── PermissionsManager.java │ │ │ └── PermissionsResultAction.java │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ └── activity_main2.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── soundwave │ └── jwkj │ └── com │ └── soundwavedemo │ └── ExampleUnitTest.java ├── build.gradle ├── demo.gif ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 声波配网demo 2 | 3 | ### 配置 4 | 5 | **Step 1.** Add the JitPack repository to your build file 6 | 7 | Add it in your root build.gradle at the end of repositories: 8 | ```java 9 | allprojects { 10 | repositories { 11 | ... 12 | maven { url 'https://jitpack.io' } 13 | } 14 | } 15 | ``` 16 | 17 | **Step 2.** Add the dependency 18 | 19 | ```java 20 | dependencies { 21 | //声波配网库 22 | compile 'com.github.jwkj:SoundWaveSender:v2.0.2' 23 | //声波配网库所依赖的udp通讯库,不可删除 24 | compile 'com.jwkj:udpsender:v2.0.1' 25 | } 26 | ``` 27 | 28 | ### 忽略文件 29 | 30 | 添加忽略文件(app/proguard-rules.pro) 31 | ```java 32 | -libraryjars libs/EMTMFSDK_0101_160914.jar 33 | -dontwarn com.lsemtmf.** 34 | -keep class com.lsemtmf.**{*; } 35 | -dontwarn com.larksmart.** 36 | -keep class com.larksmart.**{*; } 37 | ``` 38 | 39 | 40 | 41 | #### 初始化 42 | 43 | 在发送广播之前需要先初始化,建议在发送广播的前2s之前初始化,所以建议在发送广播的上一个配置页就初始化(见demo) 44 | 45 | ```java 46 | SoundWaveManager.init(this);//初始化声波配置 47 | ``` 48 | 49 | 在初始化页销毁的时候,需要将声波管理器销毁以节省系统资源 50 | 51 | ```java 52 | /** 53 | * 销毁的时候也要及时销毁 54 | */ 55 | @Override 56 | protected void onDestroy() { 57 | SoundWaveManager.onDestroy(this); 58 | } 59 | ``` 60 | #### 发送声波 61 | 62 | ```java 63 | SoundWaveSender.getInstance() 64 | .with(this)//不要忘记写哦 65 | .setWifiSet(wifiSSID, wifiPwd)//wifi名字和wifi密码 66 | .send(new ResultCallback() { 67 | /** 68 | *拿到结果的时候会回调(温馨提示:由于设备的重发机制,可能会收到多条重复数据,需自己处理哦) 69 | */ 70 | @Override 71 | public void onNext(UDPResult udpResult) { 72 | //get result 73 | } 74 | /** 75 | * 声波发送失败的时候会回调 76 | */ 77 | @Override 78 | public void onError(Throwable throwable) { 79 | //发生错误的时候需要处理一下,一般是先关闭声波发送,再重发 80 | } 81 | 82 | /** 83 | * 当声波停止的时候 84 | */ 85 | @Override 86 | public void onStopSend() { 87 | //当声波播放完成的时候会回调,此时如果还没拿到结果,那么建议在此处重新发送声波 88 | } 89 | }); 90 | ``` 91 | 92 | #### 关闭声波发送 93 | 94 | ```java 95 | SoundWaveSender.getInstance().stopSend(); 96 | ``` 97 | 98 | 99 | 此外,为了避免非正常情况退出应用导致未能及时调用stopsend()停止任务,建议在activity/fragment的生命周期销毁的时候也关闭任务 100 | 101 | ```java 102 | /** 103 | * 页面停止的时候也要及时关闭 104 | */ 105 | @Override 106 | protected void onStop() { 107 | SoundWaveSender.getInstance().with(this).stopSend(); 108 | super.onStop(); 109 | } 110 | ``` 111 | 112 | >截图 113 | 114 | ![](https://github.com/jwkj/SoundwaveDemo/blob/master/demo.gif) 115 | 116 | 117 | **温馨提示:** 118 | 119 | - 建议将手机音量调至最大 120 | 121 | - 建议将手机靠近设备30cm以内 122 | 123 | - 需要将手机连接到wifi 124 | 125 | - 暂不支持5G的wifi 126 | 127 | ## 版本更新记录 128 | 129 | ### aar版 130 | soundwave-release.aar 131 | 132 | - 【新增】添加64位支持,取消v2.0.2,以soundwave-release.aar的方式依赖 133 | 134 | ### 2.x版 135 | 2.0.2 136 | 137 | - 【修复】多次发送声波导致不走onNext的bug 138 | 139 | 2.0.1 140 | 141 | - 【修复】关闭任务还在接收结果 142 | - 【优化】更换底层库,使用外部引用方式导入,共享主项目依赖文件 143 | 144 | > 2.x 相对稳定可用于生产环境,使用方法不变。不建议使用1.x版本, 145 | 146 | 147 | ### 1.x版 148 | 149 | > 不建议使用1.x版,不太稳定,请使用最新版,使用方法不变 150 | 151 | 1.1.2 152 | 153 | - 【修复】多次调用onNext方法 154 | 155 | 156 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | buildToolsVersion "26.0.3" 6 | defaultConfig { 7 | applicationId "com.jwkj.soundwavedemo" 8 | minSdkVersion 16 9 | targetSdkVersion 26 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | ndk { 14 | //选择要添加的对应cpu类型的.so库。 15 | abiFilters 'armeabi', 'arm64-v8a' 16 | } 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | compile fileTree(include: ['*.aar'], dir: 'libs') 28 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 29 | exclude group: 'com.android.support', module: 'support-annotations' 30 | }) 31 | compile 'com.android.support:appcompat-v7:26.1.0' 32 | testCompile 'junit:junit:4.12' 33 | //日志打印 34 | compile 'com.github.huangdali:ELog:v1.3.2' 35 | //声波配网库 36 | //compile 'com.github.jwkj:SoundWaveSender:v2.0.2' 37 | //声波配网库说依赖的udp通讯库,不可删除 38 | compile 'com.jwkj:udpsender:v2.0.1' 39 | } 40 | -------------------------------------------------------------------------------- /app/libs/soundwave-release.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwkj/SoundwaveDemo/fff44a1b6d0e52e116bde01a0f235767fe4ad56c/app/libs/soundwave-release.aar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | #添加忽略文件 19 | -libraryjars libs/EMTMFSDK_0101_160914.jar 20 | -dontwarn com.lsemtmf.** 21 | -keep class com.lsemtmf.**{*; } 22 | -dontwarn com.larksmart.** 23 | -keep class com.larksmart.**{*; } -------------------------------------------------------------------------------- /app/src/androidTest/java/soundwave/jwkj/com/soundwavedemo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.jwkj.soundwavedemo; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.jwkj.soundwavedemo", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/jwkj/soundwavedemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.jwkj.soundwavedemo; 2 | 3 | import android.net.wifi.WifiInfo; 4 | import android.net.wifi.WifiManager; 5 | import android.os.Bundle; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.view.View; 8 | import android.widget.EditText; 9 | import android.widget.TextView; 10 | import android.widget.Toast; 11 | 12 | import com.hdl.elog.ELog; 13 | import com.hdl.udpsenderlib.UDPResult; 14 | import com.jwkj.soundwave.ResultCallback; 15 | import com.jwkj.soundwave.SoundWaveSender; 16 | import com.jwkj.soundwave.bean.NearbyDevice; 17 | 18 | public class MainActivity extends AppCompatActivity { 19 | private boolean isNeedSendWave = true;//是否需要发送声波,没有接到正确数据之前都需要发送哦 20 | private String wifiSSID;//wifi名字 21 | private String wifiPwd;//wifi密码 22 | private TextView tvWifiName, tvLog; 23 | private EditText etWifiPwd; 24 | 25 | @Override 26 | protected void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | setContentView(R.layout.activity_main); 29 | initView(); 30 | getWifiName(); 31 | } 32 | 33 | /** 34 | * 初始化view 35 | */ 36 | private void initView() { 37 | tvWifiName = (TextView) findViewById(R.id.tv_wifiname); 38 | tvLog = (TextView) findViewById(R.id.tv_log); 39 | etWifiPwd = (EditText) findViewById(R.id.et_wifi_pwd); 40 | } 41 | 42 | /** 43 | * 获取wifi名字 44 | * 45 | * @return 46 | */ 47 | private boolean getWifiName() { 48 | WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE); 49 | if (wifiManager.isWifiEnabled()) { 50 | WifiInfo wifiInfo = wifiManager.getConnectionInfo(); 51 | ELog.e("SSID", wifiInfo.getSSID()); 52 | tvWifiName.setText(wifiInfo.getSSID()); 53 | wifiSSID = wifiInfo.getSSID(); 54 | //去掉首尾"号 55 | if ("\"".equals(wifiSSID.substring(0, 1)) && "\"".equals(wifiSSID.substring(wifiSSID.length() - 1, wifiSSID.length()))) { 56 | wifiSSID = wifiSSID.substring(1, wifiSSID.length() - 1); 57 | } 58 | ELog.e("wifiSSID=" + wifiSSID); 59 | return true; 60 | } else { 61 | showMsg("请连接wifi"); 62 | return false; 63 | } 64 | } 65 | 66 | /** 67 | * 开始发送声波 68 | * 69 | * @param view 70 | */ 71 | public void onSend(View view) { 72 | isNeedSendWave=true;//每次点击发送都要设置为可以继续发送 73 | wifiPwd = etWifiPwd.getText().toString().trim();//记录密码 74 | tvLog.append("\n声波发送中...."); 75 | sendSoundWave(); 76 | } 77 | 78 | /** 79 | * 开始发送声波 80 | */ 81 | private void sendSoundWave() { 82 | SoundWaveSender.getInstance() 83 | .with(this) 84 | .setWifiSet(wifiSSID, wifiPwd) 85 | .send(new ResultCallback() { 86 | 87 | @Override 88 | public void onNext(UDPResult udpResult) { 89 | NearbyDevice device = NearbyDevice.getDeviceInfoByByteArray(udpResult.getResultData()); 90 | device.setIp(udpResult.getIp()); 91 | ELog.e(device.toString()); 92 | tvLog.append("\n设备联网成功:(设备信息)" + device.toString()); 93 | isNeedSendWave = false; 94 | SoundWaveSender.getInstance().stopSend();//收到数据之后,需要发送 95 | } 96 | 97 | @Override 98 | public void onError(Throwable throwable) { 99 | super.onError(throwable); 100 | ELog.e("" + throwable); 101 | SoundWaveSender.getInstance().stopSend();//出错了就要停止任务,然后重启发送 102 | } 103 | 104 | /** 105 | * 当声波停止的时候 106 | */ 107 | @Override 108 | public void onStopSend() { 109 | if (isNeedSendWave) {//是否需要继续发送声波 110 | tvLog.append("\n继续发送声波..."); 111 | sendSoundWave(); 112 | } else {//结束了就需要将发送器关闭 113 | SoundWaveSender.getInstance().stopSend(); 114 | } 115 | } 116 | }); 117 | } 118 | 119 | /** 120 | * 显示提示消息 121 | * 122 | * @param msg 123 | */ 124 | public void showMsg(String msg) { 125 | Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); 126 | } 127 | 128 | /** 129 | * 停止发送声波 130 | * 131 | * @param view 132 | */ 133 | public void onStopSoundWave(View view) { 134 | SoundWaveSender.getInstance().with(this).stopSend(); 135 | } 136 | 137 | @Override 138 | protected void onStop() { 139 | SoundWaveSender.getInstance().with(this).stopSend(); 140 | finish(); 141 | super.onStop(); 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /app/src/main/java/com/jwkj/soundwavedemo/StartActivity.java: -------------------------------------------------------------------------------- 1 | package com.jwkj.soundwavedemo; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.view.View; 7 | 8 | import com.hdl.elog.ELog; 9 | import com.jwkj.soundwave.SoundWaveManager; 10 | import com.jwkj.soundwavedemo.runtimepermissions.PermissionsManager; 11 | import com.jwkj.soundwavedemo.runtimepermissions.PermissionsResultAction; 12 | 13 | public class StartActivity extends AppCompatActivity { 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_main2); 19 | boolean isSuccess = SoundWaveManager.init(this);//初始化声波配置 20 | ELog.hdl("isSuccess=" + isSuccess); 21 | /** 22 | * 请求所有必要的权限---- 23 | */ 24 | PermissionsManager.getInstance().requestAllManifestPermissionsIfNecessary(this, new PermissionsResultAction() { 25 | @Override 26 | public void onGranted() { 27 | // Toast.makeText(MainActivity.this, "All permissions have been granted", Toast.LENGTH_SHORT).show(); 28 | } 29 | 30 | @Override 31 | public void onDenied(String permission) { 32 | //Toast.makeText(MainActivity.this, "Permission " + permission + " has been denied", Toast.LENGTH_SHORT).show(); 33 | } 34 | }); 35 | } 36 | 37 | public void onNext(View view) { 38 | startActivity(new Intent(this, MainActivity.class)); 39 | } 40 | 41 | /** 42 | * 销毁的时候也要及时销毁 43 | */ 44 | @Override 45 | protected void onDestroy() { 46 | SoundWaveManager.onDestroy(this); 47 | super.onDestroy(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/jwkj/soundwavedemo/runtimepermissions/Permissions.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Anthony Restaino 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, 9 | software distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 11 | either express or implied. See the License for the specific language governing 12 | permissions and limitations under the License. 13 | */ 14 | package com.jwkj.soundwavedemo.runtimepermissions; 15 | 16 | /** 17 | * Enum class to handle the different states 18 | * of permissions since the PackageManager only 19 | * has a granted and denied state. 20 | */ 21 | enum Permissions { 22 | GRANTED, 23 | DENIED, 24 | NOT_FOUND 25 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jwkj/soundwavedemo/runtimepermissions/PermissionsManager.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Anthony Restaino 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, 9 | software distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 11 | either express or implied. See the License for the specific language governing 12 | permissions and limitations under the License. 13 | */ 14 | package com.jwkj.soundwavedemo.runtimepermissions; 15 | 16 | import android.Manifest; 17 | import android.app.Activity; 18 | import android.content.Context; 19 | import android.content.pm.PackageInfo; 20 | import android.content.pm.PackageManager; 21 | import android.os.Build; 22 | import android.support.annotation.NonNull; 23 | import android.support.annotation.Nullable; 24 | import android.support.v4.app.ActivityCompat; 25 | import android.support.v4.app.Fragment; 26 | import android.util.Log; 27 | 28 | import java.lang.ref.WeakReference; 29 | import java.lang.reflect.Field; 30 | import java.util.ArrayList; 31 | import java.util.HashSet; 32 | import java.util.Iterator; 33 | import java.util.List; 34 | import java.util.Set; 35 | 36 | /** 37 | * A class to help you manage your permissions simply. 38 | */ 39 | public class PermissionsManager { 40 | 41 | private static final String TAG = PermissionsManager.class.getSimpleName(); 42 | 43 | private final Set mPendingRequests = new HashSet(1); 44 | private final Set mPermissions = new HashSet(1); 45 | private final List> mPendingActions = new ArrayList>(1); 46 | 47 | private static PermissionsManager mInstance = null; 48 | 49 | public static PermissionsManager getInstance() { 50 | if (mInstance == null) { 51 | mInstance = new PermissionsManager(); 52 | } 53 | return mInstance; 54 | } 55 | 56 | private PermissionsManager() { 57 | initializePermissionsMap(); 58 | } 59 | 60 | /** 61 | * This method uses reflection to read all the permissions in the Manifest class. 62 | * This is necessary because some permissions do not exist on older versions of Android, 63 | * since they do not exist, they will be denied when you check whether you have permission 64 | * which is problematic since a new permission is often added where there was no previous 65 | * permission required. We initialize a Set of available permissions and check the set 66 | * when checking if we have permission since we want to know when we are denied a permission 67 | * because it doesn't exist yet. 68 | */ 69 | private synchronized void initializePermissionsMap() { 70 | Field[] fields = Manifest.permission.class.getFields(); 71 | for (Field field : fields) { 72 | String name = null; 73 | try { 74 | name = (String) field.get(""); 75 | } catch (IllegalAccessException e) { 76 | Log.e(TAG, "Could not access field", e); 77 | } 78 | mPermissions.add(name); 79 | } 80 | } 81 | 82 | /** 83 | * This method retrieves all the permissions declared in the application's manifest. 84 | * It returns a non null array of permisions that can be declared. 85 | * 86 | * @param activity the Activity necessary to check what permissions we have. 87 | * @return a non null array of permissions that are declared in the application manifest. 88 | */ 89 | @NonNull 90 | private synchronized String[] getManifestPermissions(@NonNull final Activity activity) { 91 | PackageInfo packageInfo = null; 92 | List list = new ArrayList(1); 93 | try { 94 | Log.d(TAG, activity.getPackageName()); 95 | packageInfo = activity.getPackageManager().getPackageInfo(activity.getPackageName(), PackageManager.GET_PERMISSIONS); 96 | } catch (PackageManager.NameNotFoundException e) { 97 | Log.e(TAG, "A problem occurred when retrieving permissions", e); 98 | } 99 | if (packageInfo != null) { 100 | String[] permissions = packageInfo.requestedPermissions; 101 | if (permissions != null) { 102 | for (String perm : permissions) { 103 | Log.d(TAG, "Manifest contained permission: " + perm); 104 | list.add(perm); 105 | } 106 | } 107 | } 108 | return list.toArray(new String[list.size()]); 109 | } 110 | 111 | /** 112 | * This method adds the {@link PermissionsResultAction} to the current list 113 | * of pending actions that will be completed when the permissions are 114 | * received. The list of permissions passed to this method are registered 115 | * in the PermissionsResultAction object so that it will be notified of changes 116 | * made to these permissions. 117 | * 118 | * @param permissions the required permissions for the action to be executed. 119 | * @param action the action to add to the current list of pending actions. 120 | */ 121 | private synchronized void addPendingAction(@NonNull String[] permissions, 122 | @Nullable PermissionsResultAction action) { 123 | if (action == null) { 124 | return; 125 | } 126 | action.registerPermissions(permissions); 127 | mPendingActions.add(new WeakReference(action)); 128 | } 129 | 130 | /** 131 | * This method removes a pending action from the list of pending actions. 132 | * It is used for cases where the permission has already been granted, so 133 | * you immediately wish to remove the pending action from the queue and 134 | * execute the action. 135 | * 136 | * @param action the action to remove 137 | */ 138 | private synchronized void removePendingAction(@Nullable PermissionsResultAction action) { 139 | for (Iterator> iterator = mPendingActions.iterator(); 140 | iterator.hasNext(); ) { 141 | WeakReference weakRef = iterator.next(); 142 | if (weakRef.get() == action || weakRef.get() == null) { 143 | iterator.remove(); 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * This static method can be used to check whether or not you have a specific permission. 150 | * It is basically a less verbose method of using {@link ActivityCompat#checkSelfPermission(Context, String)} 151 | * and will simply return a boolean whether or not you have the permission. If you pass 152 | * in a null Context object, it will return false as otherwise it cannot check the permission. 153 | * However, the Activity parameter is nullable so that you can pass in a reference that you 154 | * are not always sure will be valid or not (e.g. getActivity() from Fragment). 155 | * 156 | * @param context the Context necessary to check the permission 157 | * @param permission the permission to check 158 | * @return true if you have been granted the permission, false otherwise 159 | */ 160 | @SuppressWarnings("unused") 161 | public synchronized boolean hasPermission(@Nullable Context context, @NonNull String permission) { 162 | return context != null && (ActivityCompat.checkSelfPermission(context, permission) 163 | == PackageManager.PERMISSION_GRANTED || !mPermissions.contains(permission)); 164 | } 165 | 166 | /** 167 | * This static method can be used to check whether or not you have several specific permissions. 168 | * It is simpler than checking using {@link ActivityCompat#checkSelfPermission(Context, String)} 169 | * for each permission and will simply return a boolean whether or not you have all the permissions. 170 | * If you pass in a null Context object, it will return false as otherwise it cannot check the 171 | * permission. However, the Activity parameter is nullable so that you can pass in a reference 172 | * that you are not always sure will be valid or not (e.g. getActivity() from Fragment). 173 | * 174 | * @param context the Context necessary to check the permission 175 | * @param permissions the permissions to check 176 | * @return true if you have been granted all the permissions, false otherwise 177 | */ 178 | @SuppressWarnings("unused") 179 | public synchronized boolean hasAllPermissions(@Nullable Context context, @NonNull String[] permissions) { 180 | if (context == null) { 181 | return false; 182 | } 183 | boolean hasAllPermissions = true; 184 | for (String perm : permissions) { 185 | hasAllPermissions &= hasPermission(context, perm); 186 | } 187 | return hasAllPermissions; 188 | } 189 | 190 | /** 191 | * This method will request all the permissions declared in your application manifest 192 | * for the specified {@link PermissionsResultAction}. The purpose of this method is to enable 193 | * all permissions to be requested at one shot. The PermissionsResultAction is used to notify 194 | * you of the user allowing or denying each permission. The Activity and PermissionsResultAction 195 | * parameters are both annotated Nullable, but this method will not work if the Activity 196 | * is null. It is only annotated Nullable as a courtesy to prevent crashes in the case 197 | * that you call this from a Fragment where {@link Fragment#getActivity()} could yield 198 | * null. Additionally, you will not receive any notification of permissions being granted 199 | * if you provide a null PermissionsResultAction. 200 | * 201 | * @param activity the Activity necessary to request and check permissions. 202 | * @param action the PermissionsResultAction used to notify you of permissions being accepted. 203 | */ 204 | @SuppressWarnings("unused") 205 | public synchronized void requestAllManifestPermissionsIfNecessary(final @Nullable Activity activity, 206 | final @Nullable PermissionsResultAction action) { 207 | if (activity == null) { 208 | return; 209 | } 210 | String[] perms = getManifestPermissions(activity); 211 | requestPermissionsIfNecessaryForResult(activity, perms, action); 212 | } 213 | 214 | /** 215 | * This method should be used to execute a {@link PermissionsResultAction} for the array 216 | * of permissions passed to this method. This method will request the permissions if 217 | * they need to be requested (i.e. we don't have permission yet) and will add the 218 | * PermissionsResultAction to the queue to be notified of permissions being granted or 219 | * denied. In the case of pre-Android Marshmallow, permissions will be granted immediately. 220 | * The Activity variable is nullable, but if it is null, the method will fail to execute. 221 | * This is only nullable as a courtesy for Fragments where getActivity() may yeild null 222 | * if the Fragment is not currently added to its parent Activity. 223 | * 224 | * @param activity the activity necessary to request the permissions. 225 | * @param permissions the list of permissions to request for the {@link PermissionsResultAction}. 226 | * @param action the PermissionsResultAction to notify when the permissions are granted or denied. 227 | */ 228 | @SuppressWarnings("unused") 229 | public synchronized void requestPermissionsIfNecessaryForResult(@Nullable Activity activity, 230 | @NonNull String[] permissions, 231 | @Nullable PermissionsResultAction action) { 232 | if (activity == null) { 233 | return; 234 | } 235 | addPendingAction(permissions, action); 236 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 237 | doPermissionWorkBeforeAndroidM(activity, permissions, action); 238 | } else { 239 | List permList = getPermissionsListToRequest(activity, permissions, action); 240 | if (permList.isEmpty()) { 241 | //if there is no permission to request, there is no reason to keep the action int the list 242 | removePendingAction(action); 243 | } else { 244 | String[] permsToRequest = permList.toArray(new String[permList.size()]); 245 | mPendingRequests.addAll(permList); 246 | ActivityCompat.requestPermissions(activity, permsToRequest, 1); 247 | } 248 | } 249 | } 250 | 251 | /** 252 | * This method should be used to execute a {@link PermissionsResultAction} for the array 253 | * of permissions passed to this method. This method will request the permissions if 254 | * they need to be requested (i.e. we don't have permission yet) and will add the 255 | * PermissionsResultAction to the queue to be notified of permissions being granted or 256 | * denied. In the case of pre-Android Marshmallow, permissions will be granted immediately. 257 | * The Fragment variable is used, but if {@link Fragment#getActivity()} returns null, this method 258 | * will fail to work as the activity reference is necessary to check for permissions. 259 | * 260 | * @param fragment the fragment necessary to request the permissions. 261 | * @param permissions the list of permissions to request for the {@link PermissionsResultAction}. 262 | * @param action the PermissionsResultAction to notify when the permissions are granted or denied. 263 | */ 264 | @SuppressWarnings("unused") 265 | public synchronized void requestPermissionsIfNecessaryForResult(@NonNull Fragment fragment, 266 | @NonNull String[] permissions, 267 | @Nullable PermissionsResultAction action) { 268 | Activity activity = fragment.getActivity(); 269 | if (activity == null) { 270 | return; 271 | } 272 | addPendingAction(permissions, action); 273 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 274 | doPermissionWorkBeforeAndroidM(activity, permissions, action); 275 | } else { 276 | List permList = getPermissionsListToRequest(activity, permissions, action); 277 | if (permList.isEmpty()) { 278 | //if there is no permission to request, there is no reason to keep the action int the list 279 | removePendingAction(action); 280 | } else { 281 | String[] permsToRequest = permList.toArray(new String[permList.size()]); 282 | mPendingRequests.addAll(permList); 283 | fragment.requestPermissions(permsToRequest, 1); 284 | } 285 | } 286 | } 287 | 288 | /** 289 | * This method notifies the PermissionsManager that the permissions have change. If you are making 290 | * the permissions requests using an Activity, then this method should be called from the 291 | * Activity callback onRequestPermissionsResult() with the variables passed to that method. If 292 | * you are passing a Fragment to make the permissions request, then you should call this in 293 | * the {@link Fragment#onRequestPermissionsResult(int, String[], int[])} method. 294 | * It will notify all the pending PermissionsResultAction objects currently 295 | * in the queue, and will remove the permissions request from the list of pending requests. 296 | * 297 | * @param permissions the permissions that have changed. 298 | * @param results the values for each permission. 299 | */ 300 | @SuppressWarnings("unused") 301 | public synchronized void notifyPermissionsChange(@NonNull String[] permissions, @NonNull int[] results) { 302 | int size = permissions.length; 303 | if (results.length < size) { 304 | size = results.length; 305 | } 306 | Iterator> iterator = mPendingActions.iterator(); 307 | while (iterator.hasNext()) { 308 | PermissionsResultAction action = iterator.next().get(); 309 | for (int n = 0; n < size; n++) { 310 | if (action == null || action.onResult(permissions[n], results[n])) { 311 | iterator.remove(); 312 | break; 313 | } 314 | } 315 | } 316 | for (int n = 0; n < size; n++) { 317 | mPendingRequests.remove(permissions[n]); 318 | } 319 | } 320 | 321 | /** 322 | * When request permissions on devices before Android M (Android 6.0, API Level 23) 323 | * Do the granted or denied work directly according to the permission status 324 | * 325 | * @param activity the activity to check permissions 326 | * @param permissions the permissions names 327 | * @param action the callback work object, containing what we what to do after 328 | * permission check 329 | */ 330 | private void doPermissionWorkBeforeAndroidM(@NonNull Activity activity, 331 | @NonNull String[] permissions, 332 | @Nullable PermissionsResultAction action) { 333 | for (String perm : permissions) { 334 | if (action != null) { 335 | if (!mPermissions.contains(perm)) { 336 | action.onResult(perm, Permissions.NOT_FOUND); 337 | } else if (ActivityCompat.checkSelfPermission(activity, perm) 338 | != PackageManager.PERMISSION_GRANTED) { 339 | action.onResult(perm, Permissions.DENIED); 340 | } else { 341 | action.onResult(perm, Permissions.GRANTED); 342 | } 343 | } 344 | } 345 | } 346 | 347 | /** 348 | * Filter the permissions list: 349 | * If a permission is not granted, add it to the result list 350 | * if a permission is granted, do the granted work, do not add it to the result list 351 | * 352 | * @param activity the activity to check permissions 353 | * @param permissions all the permissions names 354 | * @param action the callback work object, containing what we what to do after 355 | * permission check 356 | * @return a list of permissions names that are not granted yet 357 | */ 358 | @NonNull 359 | private List getPermissionsListToRequest(@NonNull Activity activity, 360 | @NonNull String[] permissions, 361 | @Nullable PermissionsResultAction action) { 362 | List permList = new ArrayList(permissions.length); 363 | for (String perm : permissions) { 364 | if (!mPermissions.contains(perm)) { 365 | if (action != null) { 366 | action.onResult(perm, Permissions.NOT_FOUND); 367 | } 368 | } else if (ActivityCompat.checkSelfPermission(activity, perm) != PackageManager.PERMISSION_GRANTED) { 369 | if (!mPendingRequests.contains(perm)) { 370 | permList.add(perm); 371 | } 372 | } else { 373 | if (action != null) { 374 | action.onResult(perm, Permissions.GRANTED); 375 | } 376 | } 377 | } 378 | return permList; 379 | } 380 | 381 | } -------------------------------------------------------------------------------- /app/src/main/java/com/jwkj/soundwavedemo/runtimepermissions/PermissionsResultAction.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Anthony Restaino 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, 9 | software distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 11 | either express or implied. See the License for the specific language governing 12 | permissions and limitations under the License. 13 | */ 14 | package com.jwkj.soundwavedemo.runtimepermissions; 15 | 16 | import android.content.pm.PackageManager; 17 | import android.os.Handler; 18 | import android.os.Looper; 19 | import android.support.annotation.CallSuper; 20 | import android.support.annotation.NonNull; 21 | import android.util.Log; 22 | 23 | import java.util.Collections; 24 | import java.util.HashSet; 25 | import java.util.Set; 26 | 27 | /** 28 | * This abstract class should be used to create an if/else action that the PermissionsManager 29 | * can execute when the permissions you request are granted or denied. Simple use involves 30 | * creating an anonymous instance of it and passing that instance to the 31 | * requestPermissionsIfNecessaryForResult method. The result will be sent back to you as 32 | * either onGranted (all permissions have been granted), or onDenied (a required permission 33 | * has been denied). Ideally you put your functionality in the onGranted method and notify 34 | * the user what won't work in the onDenied method. 35 | */ 36 | public abstract class PermissionsResultAction { 37 | 38 | private static final String TAG = PermissionsResultAction.class.getSimpleName(); 39 | private final Set mPermissions = new HashSet(1); 40 | private Looper mLooper = Looper.getMainLooper(); 41 | 42 | /** 43 | * Default Constructor 44 | */ 45 | public PermissionsResultAction() {} 46 | 47 | /** 48 | * Alternate Constructor. Pass the looper you wish the PermissionsResultAction 49 | * callbacks to be executed on if it is not the current Looper. For instance, 50 | * if you are making a permissions request from a background thread but wish the 51 | * callback to be on the UI thread, use this constructor to specify the UI Looper. 52 | * 53 | * @param looper the looper that the callbacks will be called using. 54 | */ 55 | @SuppressWarnings("unused") 56 | public PermissionsResultAction(@NonNull Looper looper) {mLooper = looper;} 57 | 58 | /** 59 | * This method is called when ALL permissions that have been 60 | * requested have been granted by the user. In this method 61 | * you should put all your permissions sensitive code that can 62 | * only be executed with the required permissions. 63 | */ 64 | public abstract void onGranted(); 65 | 66 | /** 67 | * This method is called when a permission has been denied by 68 | * the user. It provides you with the permission that was denied 69 | * and will be executed on the Looper you pass to the constructor 70 | * of this class, or the Looper that this object was created on. 71 | * 72 | * @param permission the permission that was denied. 73 | */ 74 | public abstract void onDenied(String permission); 75 | 76 | /** 77 | * This method is used to determine if a permission not 78 | * being present on the current Android platform should 79 | * affect whether the PermissionsResultAction should continue 80 | * listening for events. By default, it returns true and will 81 | * simply ignore the permission that did not exist. Usually this will 82 | * work fine since most new permissions are introduced to 83 | * restrict what was previously allowed without permission. 84 | * If that is not the case for your particular permission you 85 | * request, override this method and return false to result in the 86 | * Action being denied. 87 | * 88 | * @param permission the permission that doesn't exist on this 89 | * Android version 90 | * @return return true if the PermissionsResultAction should 91 | * ignore the lack of the permission and proceed with exection 92 | * or false if the PermissionsResultAction should treat the 93 | * absence of the permission on the API level as a denial. 94 | */ 95 | @SuppressWarnings({"WeakerAccess", "SameReturnValue"}) 96 | public synchronized boolean shouldIgnorePermissionNotFound(String permission) { 97 | Log.d(TAG, "Permission not found: " + permission); 98 | return true; 99 | } 100 | 101 | @SuppressWarnings("WeakerAccess") 102 | @CallSuper 103 | protected synchronized final boolean onResult(final @NonNull String permission, int result) { 104 | if (result == PackageManager.PERMISSION_GRANTED) { 105 | return onResult(permission, Permissions.GRANTED); 106 | } else { 107 | return onResult(permission, Permissions.DENIED); 108 | } 109 | 110 | } 111 | 112 | /** 113 | * This method is called when a particular permission has changed. 114 | * This method will be called for all permissions, so this method determines 115 | * if the permission affects the state or not and whether it can proceed with 116 | * calling onGranted or if onDenied should be called. 117 | * 118 | * @param permission the permission that changed. 119 | * @param result the result for that permission. 120 | * @return this method returns true if its primary action has been completed 121 | * and it should be removed from the data structure holding a reference to it. 122 | */ 123 | @SuppressWarnings("WeakerAccess") 124 | @CallSuper 125 | protected synchronized final boolean onResult(final @NonNull String permission, Permissions result) { 126 | mPermissions.remove(permission); 127 | if (result == Permissions.GRANTED) { 128 | if (mPermissions.isEmpty()) { 129 | new Handler(mLooper).post(new Runnable() { 130 | @Override 131 | public void run() { 132 | onGranted(); 133 | } 134 | }); 135 | return true; 136 | } 137 | } else if (result == Permissions.DENIED) { 138 | new Handler(mLooper).post(new Runnable() { 139 | @Override 140 | public void run() { 141 | onDenied(permission); 142 | } 143 | }); 144 | return true; 145 | } else if (result == Permissions.NOT_FOUND) { 146 | if (shouldIgnorePermissionNotFound(permission)) { 147 | if (mPermissions.isEmpty()) { 148 | new Handler(mLooper).post(new Runnable() { 149 | @Override 150 | public void run() { 151 | onGranted(); 152 | } 153 | }); 154 | return true; 155 | } 156 | } else { 157 | new Handler(mLooper).post(new Runnable() { 158 | @Override 159 | public void run() { 160 | onDenied(permission); 161 | } 162 | }); 163 | return true; 164 | } 165 | } 166 | return false; 167 | } 168 | 169 | /** 170 | * This method registers the PermissionsResultAction object for the specified permissions 171 | * so that it will know which permissions to look for changes to. The PermissionsResultAction 172 | * will then know to look out for changes to these permissions. 173 | * 174 | * @param perms the permissions to listen for 175 | */ 176 | @SuppressWarnings("WeakerAccess") 177 | @CallSuper 178 | protected synchronized final void registerPermissions(@NonNull String[] perms) { 179 | Collections.addAll(mPermissions, perms); 180 | } 181 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 17 | 18 | 25 | 26 | 30 | 31 | 38 | 39 | 40 | 48 | 49 |