标签: SDK

Android Studio中提示:Project SDK is not defined

Android Studio中提示:Project SDK is not defined
 

【背景】

之前用Android Studio去打开一个项目后,结果遇到提示:

Project SDK is not defined

如图:

android studio Project SDK is not defined

【解决过程】

1.试着去点击Setup SDK,出现设置对话框:

popup select project sdk choose android api 21 platform

然后就卡死了好一会。

然后就好了,就没了这个提示了。

 

【总结】

好像是Android Studio对于刚使用的时候,没有确定Android的SDK,所以需要去设置一下对应的所用的版本。

此处选择默认的Android API 21 Platform(java version 1.7.0_76)

即可。

其中java version 1.7.0_76是此处本地所安装的java(jdk)的版本。

MIUI 应用商店可信吗

MIUI 应用商店可信吗

 

seers · 3 天前 · 2115 次点击

之前听说过某些游戏会变成渠道服,里面的 app 会不会存在反编译加入广告 /垃圾 SDK 之类的呢

20 条回复    2021-09-09 09:59:54 +08:00

anguiao
    1

anguiao   3 天前 via Android

不存在“变成渠道服”,都是游戏厂商自愿上架的。
imn1
    2

imn1   3 天前

本站有描述他家套上广告的帖子
AoEiuV020
    3

AoEiuV020   3 天前 via Android

有个强行贴开屏广告的问题,不过不在应用商店下载应该也一样吧,好像是 MIUI 通过 app 包名判断添加的,
jfdnet
    4

jfdnet   3 天前

@AoEiuV020 你是说小米系统会在第三方应用打开时强行植入小米自己的开屏广告?“劫持开屏广告”的意思?这行为貌似闻所未闻。你有证据吗?
wangxn
    5

wangxn   3 天前

@jfdnet 这是小米和应用厂商之间光明正大的协议

lscho
    6

lscho   3 天前

@jfdnet 这个很早就有了,小米会自行覆盖一层广告,但是不知道现在整改没有
syuraking
    7

syuraking   3 天前

@AoEiuV020 卸载:com.miui.systemAdSolution,就不会有乱加的开屏广告了
chengyiqun
    8

chengyiqun   3 天前   ❤️ 1

@jfdnet 有的, 某些 app, 偶尔开屏广告被 miui 劫持, 到 sdcard 目录删除 miad 目录, 建立 miad 空文件, 就没了.
jerryjhou
    9

jerryjhou   3 天前 via Android

@lscho 广告从来不需要整改,诱导点击假按钮或内容不当才需要
Lemeng
    10

Lemeng   3 天前

不至于吧,不然等着打脸?毕竟大神多,就像之前那个电视机上传的,影响不小
lscho
    11

lscho   3 天前

@jerryjhou 广告不需要整改啊,但是你没看懂什么意思,比如我是软件开发商,我投放的开屏广告,被小米强行覆盖了一层他的广告。这种肯定是违规的。
WebKit
    12

WebKit   3 天前 via Android

渠道服务很正常啊,都是有渠道推广分成的。
wangkun025
    13

wangkun025   3 天前

@jfdnet 没听说过就去学习。不要用“闻所未闻”来质疑别人。
jerryjhou
    14

jerryjhou   3 天前 via Android

@lscho 你觉得可能吗?还反编,签名咋办?
开发者要自己同意,相当于自动替换广告联盟(或傻瓜式一键添加广告)。以常理推断小米的费用会偏低
这个行为可能构成不正当竞争(对广告商而非开发者),但广告联盟屁股更不干净
Cielsky
    15

Cielsky   3 天前 via Android

@jfdnet 不是劫持,是软件自行接入了 MIUI 的广告服务吧。
应该是通过包名加的,干掉智能服务或者关掉其悬浮窗权限就没了。
jfdnet
    16

jfdnet   3 天前

@wangkun025 所以你学习了吗?楼主说的意思是劫持。这种风险跟收益完全不成比例的行为得傻到你这样才会去干吧。
jfdnet
    17

jfdnet   3 天前

@Cielsky 对呀。不可能是像楼主表达的那样,直接劫持开屏广告了。
Zy143L
    18

Zy143L   3 天前 via Android

MIUI 商店不会给你敢反编译加广告这种事
这种事不是存在于三五年前的互联网下载入口?什么安智市场啥的?
至于渠道服那是厂家和平台协商过的
广告呢 可以看看开发者后台
可以选择是否加入推广广告,和开发者进行广告利润分成
在骂软件开屏有 MIUI 广告的时候 咋不去问问开发者为啥要开这个开关 而不是小米为啥要加这个广告
(当然也有钻空子的人 上传了 v2ray 的包名。开启了广告业务?
unco020511
    19

unco020511   2 天前   ❤️ 1

楼上说的都不是一回事,小米应用市场提供开屏广告 sdk 供你选用(这种 sdk 很多厂商都有),开发者可以用也可以不用啊,至于说不经过开发者同意,反编译后恶意加上自己的开屏广告,应该是不存在的
jerryjhou
    20

jerryjhou   1 天前 via Android

@unco020511 水军抹黑小米就这么玩的,明明都是从菊厂 copy 来的
小米弄个纯净模式骂垄断,安装器诱导跳转骂不要脸…我要是没有手头没有 V20 提前体验这些功能就真信了

现在还有能免费加固安卓 SDK 的厂商吗?

请问现在还有能免费加固安卓 SDK 的厂商吗?

 

icetea12138 · 2 天前 · 1271 次点击

网上找了很多网易之类的都不行,各位还有能用的吗?(我知道加固没用,但是甲方要求没有办法)

6 条回复    2021-09-09 09:56:23 +08:00

james2013
    1

james2013   2 天前

加固安卓 apk 的有腾讯出的乐固,安卓 SDK 加固不了,要不然别人怎么代码引用呢?只看到过安卓 SDK 代码做混淆的
ruilin10086
    2

ruilin10086   1 天前

360 加固可以
markgor
    3

markgor   1 天前   ❤️ 1

腾讯–乐固
360 –加固

基础功能免费,
两个我都有用过,
两者加固程度不好说,
单纯使用体验上我比较倾向 360 的。

腾讯的:(首次注册登录)->腾讯云->每次循环流程->加固->上传 APK->自己时不时刷新下看看是否完成->下载->重新签名
360 的:(首次注册下载客户端->设置证书、密码)->客户端上传 APK->等待加固完成通知

换句话说,360 只需要上传 apk 后,加固成功了他自己下载回本地,自己帮你签名,然后提示你完成了。

janus77
    4

janus77   1 天前

甲方有要求你们不会找甲方要预算吗
huqi
    5

huqi   1 天前

我们用的 ijiami , 不过是收费版的,费用没注意问,加固的是 apk ;他家也有免费版,但免费版过不了公司安全部门的扫描。
jerryjhou
    6

jerryjhou   1 天前 via Android

@janus77 你咋知道没要呢

SDK是什么?与API有什么关系?

SDK是一系列程序接口,文档,开发工具的集合,是的,集合,sdk即单单不是一个开发工具,也不是一个程序。一个完整的SDK应该包括以下内容:(1)接口文件和库文件(2)帮助文档(3)开发示例(4)实用工具。

SDK即“软体开发工具包”,一般是一些被软件工程师用于为特定的软件包、软件框架、硬件平台、操作系统等建立应用软件的开发工具的集合。通俗点是指由第三方服务商提供的实现软件产品某项功能的工具包。

通常SDK是由专业性质的公司提供专业服务的集合,比如提供安卓开发工具、或者基于硬件开发的服务等。也有针对某项软件功能的SDK,如推送技术、图像识别技术、移动支付技术、语音识别分析技术等,在互联网开放的大趋势下,一些功能性的SDK已经被当作一个产品来运营。

开发者不需要再对产品的每个功能进行开发,选择合适稳定的SDK服务并花费很少的经历就可以在产品中集成某项功能。

接口文件和库文件就是API,将底层的代码进行封装保护,提供给用户一个调用底层代码的接口;
帮助文档解释接口文件和库文件功能,以及介绍相关的开发工具,操作示例等等;
开发示例就是做出来的一个DEMO展示,也要包括源代码;
实用工具是用来协助用户进行二次开发的工具,比如二次开发向导、API 搜索工具、软件打包工具等。

有过java编程经历的都知道,要运行java需要在电脑上安装jdk。jdk就是java SDK ,其安装过程就是下载一个EXE(Windows下)的应用程序,点一下就OK了,看起来好像jdk就是一个应用程序。实际上这个在网上下载下来的应用程序只是jdk的一个安装向导,它帮你在电脑上安装了Java的运行环境,一堆Java工具和Java基础的类库,这些东西组合起来才是JDK的核心内容。
通过上述的示例,相信你大概明白了sdk包含些什么东西了。

那么学c的同学又会问,为什么在windows下运行c语言,没见要下载什么’C  SDK’什么的啊?
因为C语言没有特定SDK,也不需要像Java一样在虚拟机上运行程序示例。但是C语言有丰富的API,同时C语言不需要像Java一样使用Java开发商提供的API,它可以直接调用系统本身的API。

实际开发中,可以轻松的获得多种多类的软件开发的工具(如keil uvision, eclipse,Visual C++等),这些工具集成了语言的开发环境、编译环境,还提供纠错功能。可以通过这些工具将SDK中的函数,框架类导入,使用别人写好的类,协助软件的开发。

 

=============== API ===============

(1)API的概念

API即“应用程序编程接口”,是一些预先定义的函数,目的是作为“介面”沟通两个不同的东西,提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。

其实就是别人已经写好的可以实现特定功能的函数,而你只需要根据他提供好的接口,也就是调用他的方法,传入他规定的参数,然后这个函数就会帮你实现这些功能。

 

从接口interface来说,在计算机领域是指两个不同事物之间交互的地方,大可以到两个完整的不同系统,小可以到两段程序。所以这个I就这么理解。在这个基础上,人和程序交互的地方,叫做UI,user interface,所有人输入的包括鼠标键盘触摸屏声音输入都算。那么程序和程序交互的就叫做API,所有非人对非人交互都通过API进行交互,所谓交互,其实就是传递数据,触发功能。

(2)API应用案例

示例场景:假如你是一家小企业,公司网站上有一个表格是用来给客户注册预约的。你想要凭借这些预约细节信息,让客户能够自动在谷歌日程上创建活动。

API使用:这就意味着,你的网站服务器需要直接与谷歌服务器进行对话,在掌握既定细节信息的情况下,申请创建活动。之后,你的服务器就会接收到谷歌的响应并进行处理,然后将相关信息发送回浏览器,比如说向用户发送一个确认信息。

(3)API产品——现在也有公司将API包装成产品

案例:Weather Underground出售其天气数据API的访问权限给其他人。

(4)API的分类

API又分为(Windows、Linux、Unix等系统的)系统级API,及非操作系统级的自定义API。作为一种有效的代码封装模式,微软Windows的API开发模式已经为许多商业应用开发的公司所借鉴,并开发出某些商业应用系统的API函数予以发布,方便第三方进行功能扩展。如Google、苹果电脑公司,以及诺基亚等手机开发的API等等。

API又分为开放式API和私有API。顾名思义,开放式API即是向所有人公开的接口,允许任何人调用它并获取到它背后的数据,有时公司会将 API 作为其公共开放系统,也就是说,公司制定自己的系统接口标准,当需要执行系统整合、自定义和程序应用等操作时,公司所有成员都可以通过该接口标准调用源代码,该接口标准被称之为开放式API。私有API即接口未对外开放。

 

 

========== SDK 和 API 的关系 ==========

SDK相当于开发集成工具环境,API就是数据接口。在SDK环境下调用API数据。

实际上SDK包含了API的定义,API定义一种能力,一种接口的规范,而SDK可以包含这种能力、包含这种规范。但是SDK又不完完全全只包含API以及API的实现,它是一个软件工具包,它还有很多其他辅助性的功能。

SDK 包含了使用 API 的必需资料,所以人们也常把仅使用 API 来编写 Windows 应用程序的开发方式叫做“SDK编程”。

通俗语言解释

API

前端调用后端数据的一个通道,就是我们俗说的接口,通过这个通道,可以访问到后端的数据,但是又无需调用源代码。

SDK

工程师为辅助开发某类软件的相关文档、范例和工具的集合,使用SDK可以提高开发效率,更简单的接入某个功能。

举例说明:一个产品想实现某个功能,可以找到相关的SDK,工程师直接接入SDK,就不用再重新开发了。

 

========== 举个例子 ===========

在这里,一个简单的功能链条我将它分为三个组成部分:

1、客户端组装数据

2、客户端使用组装的数据来请求服务端(或者操作系统)的 api

3、服务端(或者操作系统)的 api 处理数据并返回处理结果

结合这个链条得出结论:

1、api为细粒度的功能接口

2、sdk包含第2、3步

3、sdk为api的集合

Android 自己实现 NavigationView [Design Support Library]

Android 自己实现 NavigationView [Design Support Library]

一、概述
Google I/O 2015 给大家带来了Android Design Support Library,对于希望做md风格的app的来说,简直是天大的喜讯了~大家可以通过Android Design Support Library该文章对其进行了解,也可以直接在github上下载示例代码运行学习。为了表达我心中的喜悦,我决定针对该库写一系列的文章来分别介绍新增加的控件。

ok,那么首先介绍的就是NavigationView。

注意下更新下as的SDK,然后在使用的过程中,在build.gradle中添加:

compile ‘com.android.support:design:22.2.0’
1
在md风格的app中,例如如下风格的侧滑菜单非常常见:

%title插图%num

在之前的设计中,你可能需要考虑如何去布局实现,例如使用ListView;再者还要去设计Item的选中状态之类~~

but,现在,google提供了NavigationView,你只需要写写布局文件,这样的效果就ok了,并且兼容到Android 2.1,非常值得去体验一下。接下来我们来介绍如何去使用这个NavigationView!

二、使用
使用起来very simple ,主要就是写写布局文件~

(一)布局文件
<?xml version=”1.0″ encoding=”utf-8″?>
<android.support.v4.widget.DrawerLayout
android:id=”@+id/id_drawer_layout”
xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:app=”http://schemas.android.com/apk/res-auto”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:fitsSystemWindows=”true”
>

<RelativeLayout
android:layout_width=”match_parent”
android:layout_height=”match_parent”>

<android.support.v7.widget.Toolbar
android:id=”@+id/id_toolbar”
android:layout_width=”match_parent”
android:layout_height=”?attr/actionBarSize”
android:background=”?attr/colorPrimary”
app:layout_scrollFlags=”scroll|enterAlways”
app:popupTheme=”@style/ThemeOverlay.AppCompat.Light”/>

<TextView
android:id=”@+id/id_tv_content”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_centerInParent=”true”
android:text=”HelloWorld”
android:textSize=”30sp”/>
</RelativeLayout>

<android.support.design.widget.NavigationView
android:id=”@+id/id_nv_menu”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:layout_gravity=”left”
android:fitsSystemWindows=”true”
app:headerLayout=”@layout/header_just_username”
app:menu=”@menu/menu_drawer”
/>

</android.support.v4.widget.DrawerLayout>

可以看到我们的*外层是DrawerLayout,里面一个content,一个作为drawer。我们的drawer为NavigationView。
注意这个view的两个属性app:headerLayout=”@layout/header_just_username”和app:menu=”@menu/menu_drawer”,分别代表drawer布局中的header和menuitem区域,当然你可以根据自己的情况使用。

接下来看看header的布局文件和menu配置文件:

<?xml version=”1.0″ encoding=”utf-8″?>
<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”192dp”
android:background=”?attr/colorPrimaryDark”
android:orientation=”vertical”
android:padding=”16dp”
android:theme=”@style/ThemeOverlay.AppCompat.Dark”>

<TextView
android:id=”@+id/id_link”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_alignParentBottom=”true”
android:layout_marginBottom=”16dp”
android:text=”http://blog.csdn.net/lmj623565791″/>

<TextView
android:id=”@+id/id_username”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_above=”@id/id_link”
android:text=”Zhang Hongyang”/>

<ImageView
android:layout_width=”72dp”
android:layout_height=”72dp”
android:layout_above=”@id/id_username”
android:layout_marginBottom=”16dp”
android:src=”@mipmap/icon”/>

</RelativeLayout>

<?xml version=”1.0″ encoding=”utf-8″?>
<menu xmlns:android=”http://schemas.android.com/apk/res/android”>

<group android:checkableBehavior=”single”>
<item
android:id=”@+id/nav_home”
android:icon=”@drawable/ic_dashboard”
android:title=”Home”/>
<item
android:id=”@+id/nav_messages”
android:icon=”@drawable/ic_event”
android:title=”Messages”/>
<item
android:id=”@+id/nav_friends”
android:icon=”@drawable/ic_headset”
android:title=”Friends”/>
<item
android:id=”@+id/nav_discussion”
android:icon=”@drawable/ic_forum”
android:title=”Discussion”/>
</group>

<item android:title=”Sub items”>
<menu>
<item
android:icon=”@drawable/ic_dashboard”
android:title=”Sub item 1″/>
<item
android:icon=”@drawable/ic_forum”
android:title=”Sub item 2″/>
</menu>
</item>

</menu>

别放错文件夹哈~

布局文件写完了,基本就好了,是不是很爽~看似复杂的效果,写写布局文件就ok。

ps:默认的颜色很多是从当前的主题中提取的,比如icon的stateColor,当然你也可以通过以下属性修改部分样式:

app:itemIconTint=””
app:itemBackground=””
app:itemTextColor=””

(二)Activity
*后是Activity:

package com.imooc.testandroid;

import android.os.Bundle;
import android.support.design.widget.NavigationView;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;

public class NavigationViewActivity extends ActionBarActivity
{

private DrawerLayout mDrawerLayout;
private NavigationView mNavigationView;

@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_navigation_view);

mDrawerLayout = (DrawerLayout) findViewById(R.id.id_drawer_layout);
mNavigationView = (NavigationView) findViewById(R.id.id_nv_menu);

Toolbar toolbar = (Toolbar) findViewById(R.id.id_toolbar);
setSupportActionBar(toolbar);

final ActionBar ab = getSupportActionBar();
ab.setHomeAsUpIndicator(R.drawable.ic_menu);
ab.setDisplayHomeAsUpEnabled(true);

setupDrawerContent(mNavigationView);

}

private void setupDrawerContent(NavigationView navigationView)
{
navigationView.setNavigationItemSelectedListener(

new NavigationView.OnNavigationItemSelectedListener()
{

@Override
public boolean onNavigationItemSelected(MenuItem menuItem)
{
menuItem.setChecked(true);
mDrawerLayout.closeDrawers();
return true;
}
});
}

@Override
public boolean onCreateOptionsMenu(Menu menu)
{
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_navigation_view, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item)
{
if(item.getItemId() == android.R.id.home)
{
mDrawerLayout.openDrawer(GravityCompat.START);
return true ;
}
return super.onOptionsItemSelected(item);
}

}

我们在Activity里面可以通过navigationView去navigationView.setNavigationItemSelectedListener,当selected的时候,menuItem去setChecked(true)。

别忘了设置theme~

<resources>

<!– Base application theme. –>
<style name=”AppTheme” parent=”Theme.AppCompat.Light.DarkActionBar”>
<!– Customize your theme here. –>
</style>

<style name=”Theme.DesignDemo” parent=”Base.Theme.DesignDemo”>
</style>

<style name=”Base.Theme.DesignDemo” parent=”Theme.AppCompat.Light.NoActionBar”>
<item name=”colorPrimary”>#673AB7</item>
<item name=”colorPrimaryDark”>#512DA8</item>
<item name=”colorAccent”>#FF4081</item>
<item name=”android:windowBackground”>@color/window_background</item>
</style>

</resources>

<color name=”window_background”>#FFF5F5F5</color>

<activity
android:name=”.NavigationViewActivity”
android:label=”@string/title_activity_navigation_view”
android:theme=”@style/Theme.DesignDemo”>
</activity>

ok,到此就搞定了~~

不过存在一个问题,此时你如果点击Sub items里面的Sub item,如果你期望当前选中应该是Sub item,你会发现不起作用。那怎么办呢?

(三)Sub Item支持Cheable
这里可以修改menu的配置文件:

<?xml version=”1.0″ encoding=”utf-8″?>
<menu xmlns:android=”http://schemas.android.com/apk/res/android”>

<group>
<item
android:id=”@+id/nav_home”
android:checkable=”true”
android:icon=”@drawable/ic_dashboard”
android:title=”Home”/>
<item
android:id=”@+id/nav_messages”
android:checkable=”true”
android:icon=”@drawable/ic_event”
android:title=”Messages”/>
<item
android:id=”@+id/nav_friends”
android:checkable=”true”
android:icon=”@drawable/ic_headset”
android:title=”Friends”/>
<item
android:id=”@+id/nav_discussion”
android:checkable=”true”
android:icon=”@drawable/ic_forum”
android:title=”Discussion”/>
</group>

<item android:title=”Sub items”>
<menu>
<item
android:checkable=”true”
android:icon=”@drawable/ic_dashboard”
android:title=”Sub item 1″/>
<item
android:checkable=”true”
android:icon=”@drawable/ic_forum”
android:title=”Sub item 2″/>
</menu>
</item>

</menu>

将android:checkableBehavior=”single”去掉,然后给每个item项添加android:checkable=”true”。

然后在代码中:

navigationView.setNavigationItemSelectedListener(

new NavigationView.OnNavigationItemSelectedListener()
{

private MenuItem mPreMenuItem;

@Override
public boolean onNavigationItemSelected(MenuItem menuItem)
{
if (mPreMenuItem != null) mPreMenuItem.setChecked(false);
menuItem.setChecked(true);
mDrawerLayout.closeDrawers();
mPreMenuItem = menuItem;
return true;
}
});

存一下preMenuItem,手动切换。

效果:%title插图%num

 

ok,哈~其实这个还是参考链接2里面的一个评论说的~~

到此用法就介绍完了有木有一点小激动~ 但是还有个问题,对于app,就像ActionBar*初的出现,一开始大家都欢欣鼓舞,后来发现app中多数情况下需要去定制,尼玛,是不是忽然觉得ActionBar太死板了,恶心死了(当然了现在有了ToolBar灵活度上好多了)对于上述NavigationView可能也会存在定制上的问题,比如我希望选中的Item左边有个高亮的竖线之类的效果。那么,针对于各种需求,想要能解决各种问题,*好的方式就是说对于NavigationView的效果自己可以实现。*好,我们就来看看NavigationView自己实现有多难?

三、自己实现NavigationView效果
其实NavigationView的实现非常简单,一个ListView就可以了,甚至都不需要去自定义,简单写一个Adapter就行了~~

%title插图%num

首先观察该图,有没有发现神奇之处,恩,你肯定发现不了,因为我们做的太像了。

其实这个图就是我通过ListView写的一个~是不是和原版很像(~哈~参考了源码的实现,当然像。)

接下来分析,如果说是ListView,那么Item的type肯定不止一种,那我们数一数种类:

带图标和文本的
仅仅是文本的 Sub Items
分割线
你会说还有顶部那个,顶部是headview呀~~

这么分析完成,是不是瞬间觉得没有难度了~

(一)首先布局文件
<?xml version=”1.0″ encoding=”utf-8″?>
<android.support.v4.widget.DrawerLayout
android:id=”@+id/id_drawer_layout”
xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:app=”http://schemas.android.com/apk/res-auto”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:fitsSystemWindows=”true”
>

<RelativeLayout
android:layout_width=”match_parent”
android:layout_height=”match_parent”>

<android.support.v7.widget.Toolbar
android:id=”@+id/id_toolbar”
android:layout_width=”match_parent”
android:layout_height=”?attr/actionBarSize”
android:background=”?attr/colorPrimary”
app:layout_scrollFlags=”scroll|enterAlways”
app:popupTheme=”@style/ThemeOverlay.AppCompat.Light”/>

<TextView
android:id=”@+id/id_tv_content”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_centerInParent=”true”
android:text=”HelloWorld”
android:textSize=”30sp”/>
</RelativeLayout>

<ListView
android:id=”@+id/id_lv_left_menu”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:layout_gravity=”start”
android:paddingTop=”0dp”
android:background=”#ffffffff”
android:clipToPadding=”false”
android:divider=”@null”
android:listSelector=”?attr/selectableItemBackground”
/>

</android.support.v4.widget.DrawerLayout>

布局文件上:和上文对比,我们仅仅把NavigationView换成了ListView.

下面注意了,一大波代码来袭.

(二) Activity
package com.imooc.testandroid;

public class NavListViewActivity extends ActionBarActivity
{
private ListView mLvLeftMenu;
private DrawerLayout mDrawerLayout;

@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_nav_list_view);

mDrawerLayout = (DrawerLayout) findViewById(R.id.id_drawer_layout);
mLvLeftMenu = (ListView) findViewById(R.id.id_lv_left_menu);

Toolbar toolbar = (Toolbar) findViewById(R.id.id_toolbar);
setSupportActionBar(toolbar);

final ActionBar ab = getSupportActionBar();
ab.setHomeAsUpIndicator(R.drawable.ic_menu);
ab.setDisplayHomeAsUpEnabled(true);

setUpDrawer();
}

private void setUpDrawer()
{
LayoutInflater inflater = LayoutInflater.from(this);
mLvLeftMenu.addHeaderView(inflater.inflate(R.layout.header_just_username, mLvLeftMenu, false));
mLvLeftMenu.setAdapter(new MenuItemAdapter(this));
}

@Override
public boolean onCreateOptionsMenu(Menu menu)
{
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_nav_list_view, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item)
{
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();

//noinspection SimplifiableIfStatement
if (id == android.R.id.home)
{
mDrawerLayout.openDrawer(GravityCompat.START);
return true;
}

return super.onOptionsItemSelected(item);
}

}

直接看onCreate中的setUpDrawer(),可以看到我们首先去addHeadView,然后去setAdapter。

那么核心代码就是我们的Adapter了~~

(三)MenuItemAdapter

public class LvMenuItem
{
public LvMenuItem(int icon, String name)
{
this.icon = icon;
this.name = name;

if (icon == NO_ICON && TextUtils.isEmpty(name))
{
type = TYPE_SEPARATOR;
} else if (icon == NO_ICON)
{
type = TYPE_NO_ICON;
} else
{
type = TYPE_NORMAL;
}

if (type != TYPE_SEPARATOR && TextUtils.isEmpty(name))
{
throw new IllegalArgumentException(“you need set a name for a non-SEPARATOR item”);
}

L.e(type + “”);

}

public LvMenuItem(String name)
{
this(NO_ICON, name);
}

public LvMenuItem()
{
this(null);
}

private static final int NO_ICON = 0;
public static final int TYPE_NORMAL = 0;
public static final int TYPE_NO_ICON = 1;
public static final int TYPE_SEPARATOR = 2;

int type;
String name;
int icon;

}

public class MenuItemAdapter extends BaseAdapter
{
private final int mIconSize;
private LayoutInflater mInflater;
private Context mContext;

public MenuItemAdapter(Context context)
{
mInflater = LayoutInflater.from(context);
mContext = context;

mIconSize = context.getResources().getDimensionPixelSize(R.dimen.drawer_icon_size);//24dp
}

private List<LvMenuItem> mItems = new ArrayList<LvMenuItem>(
Arrays.asList(
new LvMenuItem(R.drawable.ic_dashboard, “Home”),
new LvMenuItem(R.drawable.ic_event, “Messages”),
new LvMenuItem(R.drawable.ic_headset, “Friends”),
new LvMenuItem(R.drawable.ic_forum, “Discussion”),
new LvMenuItem(),
new LvMenuItem(“Sub Items”),
new LvMenuItem(R.drawable.ic_dashboard, “Sub Item 1”),
new LvMenuItem(R.drawable.ic_forum, “Sub Item 2″)
));

@Override
public int getCount()
{
return mItems.size();
}

@Override
public Object getItem(int position)
{
return mItems.get(position);
}

@Override
public long getItemId(int position)
{
return position;
}

@Override
public int getViewTypeCount()
{
return 3;
}

@Override
public int getItemViewType(int position)
{
return mItems.get(position).type;
}

@Override
public View getView(int position, View convertView, ViewGroup parent)
{
LvMenuItem item = mItems.get(position);
switch (item.type)
{
case LvMenuItem.TYPE_NORMAL:
if (convertView == null)
{
convertView = mInflater.inflate(R.layout.design_drawer_item, parent,
false);
}
TextView itemView = (TextView) convertView;
itemView.setText(item.name);
Drawable icon = mContext.getResources().getDrawable(item.icon);
setIconColor(icon);
if (icon != null)
{
icon.setBounds(0, 0, mIconSize, mIconSize);
TextViewCompat.setCompoundDrawablesRelative(itemView, icon, null, null, null);
}

break;
case LvMenuItem.TYPE_NO_ICON:
if (convertView == null)
{
convertView = mInflater.inflate(R.layout.design_drawer_item_subheader,
parent, false);
}
TextView subHeader = (TextView) convertView;
subHeader.setText(item.name);
break;
case LvMenuItem.TYPE_SEPARATOR:
if (convertView == null)
{
convertView = mInflater.inflate(R.layout.design_drawer_item_separator,
parent, false);
}
break;
}

return convertView;
}

public void setIconColor(Drawable icon)
{
int textColorSecondary = android.R.attr.textColorSecondary;
TypedValue value = new TypedValue();
if (!mContext.getTheme().resolveAttribute(textColorSecondary, value, true))
{
return;
}
int baseColor = mContext.getResources().getColor(value.resourceId);
icon.setColorFilter(baseColor, PorterDuff.Mode.MULTIPLY);
}
}

首先我们的每个Item对应于一个LvMenuItem,包含icon、name、type,可以看到我们的type分为3类。

那么adapter中代码就很简单了,多Item布局的写法。

这样就完成了,我们自己去书写NavigationView,是不是很简单~~如果你的app需要类似效果,但是又与NavigationView的效果并非一模一样,可以考虑按照上面的思路自己写一个。

*后贴一下用到的Item的布局文件:

design_drawer_item_subheader.xml
<?xml version=”1.0″ encoding=”utf-8″?>
<TextView xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”?attr/listPreferredItemHeightSmall”
android:gravity=”center_vertical|start”
android:maxLines=”1″
android:paddingLeft=”?attr/listPreferredItemPaddingLeft”
android:paddingRight=”?attr/listPreferredItemPaddingRight”
android:textAppearance=”?attr/textAppearanceListItem”
android:textColor=”?android:textColorSecondary”/>

design_drawer_item_separator.xml

<?xml version=”1.0″ encoding=”utf-8″?>
<FrameLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”>

<View android:layout_width=”match_parent”
android:layout_height=”1dp”
android:background=”?android:attr/listDivider”/>

</FrameLayout>

design_drawer_item,xml:
<?xml version=”1.0″ encoding=”utf-8″?>
<TextView
xmlns:android=”http://schemas.android.com/apk/res/android”
android:layout_width=”match_parent”
android:layout_height=”?attr/listPreferredItemHeightSmall”
android:paddingLeft=”?attr/listPreferredItemPaddingLeft”
android:paddingRight=”?attr/listPreferredItemPaddingRight”
android:drawablePadding=”32dp”
android:gravity=”center_vertical|start”
android:maxLines=”1″
android:textAppearance=”?attr/textAppearanceListItem”
android:textColor=”?android:attr/textColorPrimary”/>

ok,其实上述ListView的写法也正是NavigationView的源码实现~~item的布局文件直接从源码中拖出来的,还是爽爽哒~

源码点击下载
~~hava a nice day ~~

Bugly使用篇之Java错误堆栈还原

前面介绍了 Android混淆代码错误堆栈还原,相信大家已经知道如何通过Retrace在本地进行混淆代码还原了,上一篇提到,如果崩溃异常很多,你总不能一个一个去手动还原吧,不觉得这样做很没有效率么,有没有想过如果能实现线上监控崩溃并且能上传mapping文件进行快速还原,而不需要自己手动去做这样的一件事?没错,Bugly就是这样的一个平台,可以很方便快捷实现你这样的需求,能帮助到你提高开发效率,更加敏捷。本篇文章就跟大家分享如何使用Bugly进行错误堆栈还原。

集成Bugly
关于如何集成Bugly SDK这里不详细说明,可以到官网查看我们的SDK使用指南。

前面我也写过一篇文章快速集成Bugly Android SDK,可以参考下。

Bugly混淆配置
# 请避免混淆Bugly,在Proguard混淆文件中增加以下配置:
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}

# 保留源文件名及行号
-keepattributes SourceFile,LineNumberTable

mapping文件
Android混淆代码错误堆栈还原,这篇文章已经说过mapping文件生成的目录,它主要用来对于我们混淆过后的代码进行还原,里面列出了原始的类,方法和字段名与混淆后代码间的映射。可以举个例子:

%title插图%num

可以通过这个映射表知道我们编写的代码大致会被混淆成什么样子,我们每次发布一个版本*好要保留一份Release版的mapping文件,这样我们就可以针对不同的版本进行还原,也能更好的定位问题。

线上还原
通过集成我们Bugly SDK,就能在线上监控你的app的崩溃情况,一有崩溃发生就会上报到平台,我们制造一个Crash,看它在Bugly平台的表现:

%title插图%num

在崩溃分析可以看到Demo上报的一条异常,而这个异常的代码是被混淆过后的,这时我们需要对它进行还原。点击异常进入异常详情页,找到符号表并上传:

%title插图%num

上传成功之后,我们刷新页面就可以看到解析的结果:

%title插图%num

以后在这个版本出现的异常都能通过这个mapping文件进行堆栈还原了。

这里有个问题,每次都要上传mapping文件会不会很麻烦,能不能实现自动上传符号表?当然可以,Bugly早已帮你实现自动上传符号表的插件,详情的话看符号表配置。

总结
对代码进行混淆可以减少被破解的风险,也能达到对代码优化的作用,但如果发生了崩溃了就比较难定位问题,不过android中可以通过mapping文件进行反推,人工来做这件事的话会比较费时,所以使用Bugly能够让用户上传mapping文件来进行线上还原无疑是减少了开发同学的工作量,也能更有效的定位问题,因为不仅仅只是堆栈哦,也提供了很多辅助信息能帮组到开放同学解决问题。没有试过的同学,赶紧试试吧,老是崩溃的程序会影响产品的口碑,自然也影响你的升职加薪,不信的话,就试试吧,哈哈。

Bugly升级SDK适配Android N

前几天有个用户在我们论坛反馈一个问题,说他们的app在Android N机型中升级失败了,看了一下反馈的问题,基本确定了是因为Android N收敛了访问共享文件权限,即在Android N中使用intent不允许跨package共享file://URI,如果在工程中设置targetSDK版本为Android N并且有通过Intent传递文件它会抛出FileUriExposedException异常。发现这个问题之后呢,我自然尝试复现一下,由于没有Android 7.0的真机,我就在优测线上租用了一个7.0设备,发现我们SDK在Android 7.0在下载文件完成安装的时候就出现问题了。大家如果以后遇到类似的问题,可以利用优测的云真机来解决没有真机的痛点,节省了成本也提高了效率。

问题所在
前面已经把问题进行了一下描述,我们可以看下出错的代码:

Intent i = new Intent(Intent.ACTION_VIEW);
i.setDataAndType(Uri.fromFile(file), “application/vnd.android.package-archive”);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);

这段代码的意思是,通过intent设置数据和类型,然后通过context在新的task中启动安装apk的程序。
我们看到intent设置数据时,传递的是一个Uri,这个在API<24是没有问题的,但在Android N已经禁止你对外公开file://URI.所以我们SDK的问题就出自Uri.fromFile(file)获取uri的时候。

如何解决?
Android N已经给出明确解决方案,如果你的程序需要在应用间共享文件,您应发送一项 content://URI,并授予 URI 临时访问权限。进行此授权的*简单方式是使用 FileProvider类。

首先在AndroidManifest中注册FileProvider
代码示例:
<provider
android:name=”android.support.v4.content.FileProvider”
android:authorities=”com.bugly.upgrade.demo.fileProvider”
android:exported=”false”
android:grantUriPermissions=”true”>
<meta-data
android:name=”android.support.FILE_PROVIDER_PATHS”
android:resource=”@xml/provider_paths”/>
</provider>

这里要注意一下,FileProvider是support-v4包里面的,所以在你的程序必须要引入support-v4包。
我们可以看到在provider中需要配置相应的meta-data,这个是共享文件的路径,在res目录下新建xml文件夹并新建对应的xml文件(如下面的provider_paths),如下所示:

<?xml version=”1.0″ encoding=”utf-8″?>
<paths xmlns:android=”http://schemas.android.com/apk/res/android”>
<!– /storage/emulated/0/Download/com.bugly.upgrade.demo/.beta/apk–>
<external-path name=”beta_external_apk” path=”Download/com.bugly.upgrade.demo/.beta/apk/”/>
<!–/storage/emulated/0/Android/data/com.bugly.upgrade.demo/files/apk/–>
<external-path name=”beta_external_apk2″ path=”Android/data/com.bugly.upgrade.demo/files/apk/”/>
</paths>

%title插图%num
name表示一个URI路径段,path表示指定要分享路径的子目录。可以看到我配置了两个external-path,这两个路径都是beta下载的文件可能存在的路径,举个例子,*个路径存在的uri如下:
content://com.bugly.upgrade.demo.fileProvider/beta_external_apk/combuglyupgradedemo_21_d778bda9-f3c5-4608-b5ea-2df2a2372f91.apk。
可以看到我们配置的beta_external_apk成为了URI的一个路径段。

我们还可以指定以下路径:

<files-path name=”name” path=”path” />

表示路径在应用中的内部存储区域中files目录下的子目录下,files-path表示Context.getFilesDir()的根目录。
例如:/data/data/com.bugly.upgrade.demo/files

<cache-path name=”name” path=”path” />

表示路径在应用中红内部存储区域中cache目录下的子目录下,cache-path表示Context.getCacheDir()的根目录。
例如:/data/data/com.bugly.upgrade.demo/cache

<external-path name=”name” path=”path” />

表示路径在外部存储区域根目录的子目录,external-path表示Environment.getExternalStorageDirectory()的根目录。
例如:/storage/emulated/0

<external-cache-path name=”name” path=”path” />

表示路径在外部存储区域根目录的缓存目录,external-cache-path表示Context.getExternalCacheDir()。
例如:/storage/emulated/0/Android/data/com.bugly.upgrade.demo/cache

通过FileProvider获取Uri路径
示例代码:
Uri uri = Uri.fromFile(file);

可以更改为:

Uri uri = FileProvider.getUriForFile(context,
BuildConfig.APPLICATION_ID + “.fileProvider”, file);

因为我们SDK不会引入support-v4包,所以不能通过上面这种方式直接获取uri,*后考虑通过反射来调用getUriForFile方法,具体实现如下:

Intent i = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >= 24) {
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 反射获取FileProvider类型
Class<?> clazz = Class.forName(“android.support.v4.content.FileProvider”);
if (clazz == null) {
ELog.error(“can’t find class android.support.v4.content.FileProvider”);
return false;
}

// 通过反射调用FileProvider.getUriForFile
Uri contentUri = (Uri) Utils.invokeReflectMethod(“android.support.v4.content.FileProvider”, “getUriForFile”
, null, new Class[]{Context.class, String.class, File.class},
new Object[]{context, ComInfoManager.getCommonInfo(context).boundID + “.fileProvider”, file});

if (contentUri == null) {
ELog.error(“file location is ” + file.toString());
ELog.error(“install failed, contentUri is null!”);
return false;
}
i.setDataAndType(contentUri, “application/vnd.android.package-archive”);
} else {
i.setDataAndType(Uri.fromFile(file), “application/vnd.android.package-archive”);
}

i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);

调用反射方法定义如下:

public static Object invokeReflectMethod(String className, String methodName, Object instance,
Class<?>[] paramTypes, Object[] params) {
try {
Class<?> clazz = Class.forName(className);
Method method = clazz.getDeclaredMethod(methodName, paramTypes);
method.setAccessible(true);
return method.invoke(instance, params);
} catch (Exception e) {
return null;
}
}

大致的解决方案就如上所示啦,已经在Android 7.0验证通过了,由于在Android 7.0以上强制要求配置FileProvider,但考虑API低于24以下还是沿用之前的方法,所以只在API高于24才会去使用FileProvider。

总结
关于Android N共享文件权限的适配已经完成,还有其他特性还需要我们去验证看是否存在一些bug,其实Android每一个版本的发布都会面临这样一个问题,所以我们去了解每个版本特性的变化还是很有必要的,每次更新targetSdkVersion的时候,*好的实践就是根据特性变化列一个checklist来进行适配。好了,本篇文章内容就这么多,可能有讲得不清楚的地方,请见谅。

Android端实现多人音视频聊天应用(一)

声网Agora.io的SDK让App和网站都可以实现高质量的音频通话、视频通话、全互动直播。我试着通过该SDK实现一个多人视频通话应用。本文先分享集成与一对一视频通话的部分。

环境

声网Agora.io SDK的兼容性良好,对硬件设备和软件系统的要求不高,开发环境和测试环境满足以下条件即可:

  • Android SDK API Level >= 16
  • Android Studio 2.0 或以上版本
  • 支持语音和视频功能的真机
  • App 要求 Android 4.1 或以上设备

以下是我试用声网Agora.io SDK的开发环境和测试环境:

  • 开发环境
  • Windows 10 家庭中文版
  • Java Version SE 8
  • Android Studio 3.2 Canary 4

测试环境

  • Samsung Nexus (Android 4.4.2 API 19)
  • Mi Note 3 (Android 7.1.1 API 25)

集成

步骤一:首先点此下载完整的SDK和官方demo

步骤二:既然我们要把声网Agora.io集成到自己的项目里,所以不必运行sample,我们自己新建一个HelloAgora项目,注意一定要支持C++哦。

步骤三:把libs文件夹里的arm64-v8a、、armeabi-v7a以及x86文件夹复制粘贴到app module的libs里。如果有NDK开发的必要,则把libs->include文件夹里的两个.h头文件复制粘贴到合适位置。

步骤四:首先在app module的build.gradle文件的android代码块中添加如下代码:

  1. sourceSets {
  2. main {
  3. jniLibs.srcDirs = [‘../../../libs’]
  4. }
  5. }
  6. 复制代码

然后在app module的build.gradle文件的android->defaultConfig代码块中添加如下代码:

  1. ndk {
  2. abiFilters “armeabi-v7a”, “x86”
  3. }
  4. 复制代码

接下来在app module的build.gradle文件的dependencies代码块中添加如下代码:

  1. compile ‘io.agora.rtc:full-sdk:2.0.0’
  2. 复制代码

如果用复制粘贴jar的方式,那么此处添加如下代码:

  1. compile fileTree(dir: ‘../../../libs’, include: [‘*.jar’])
  2. 复制代码

如果有自定义NDK的必要,可以继续在app module的build.gradle文件的android代码块中添加如下代码:

  1. externalNativeBuild {
  2. ndkBuild {
  3. path ‘src/main/cpp/Android.mk’
  4. }
  5. }
  6. 复制代码

然后在app module的build.gradle文件的android->defaultConfig代码块中添加如下代码:

  1. externalNativeBuild {
  2. ndkBuild {
  3. arguments “NDK_APPLICATION_MK:=src/main/cpp/Application.mk”
  4. }
  5. }
  6. 复制代码

*后sync一下,声网Agora.io的SDK就集成到项目中来了。

权限

SDK集成完毕后,为了保证SDK能正常运行,我们需要在AndroidManisfest.xml 文件中声明以下权限:

  1. <!–允许程序连接网络–>
  2. <uses-permission android:name=“android.permission.INTERNET” />
  3. <!–允许程序录制音频–>
  4. <uses-permission android:name=“android.permission.RECORD_AUDIO” />
  5. <!–允许程序使用照相设备–>
  6. <uses-permission android:name=“android.permission.CAMERA” />
  7. <!–允许程序修改全局音频设置–>
  8. <uses-permission android:name=“android.permission.MODIFY_AUDIO_SETTINGS” />
  9. <!–允许程序获取网络状态–>
  10. <uses-permission android:name=“android.permission.ACCESS_NETWORK_STATE” />
  11. <!–允许对存储空间进行读写–>
  12. <uses-permission android:name=“android.permission.WRITE_EXTERNAL_STORAGE” />
  13. <!–允许程序连接到已配对的蓝牙设备–>
  14. <uses-permission android:name=“android.permission.BLUETOOTH” />
  15. 复制代码

这些权限都是Android开发过程中的常见权限,有经验的程序员都会感觉眼熟,WRITE_EXTERNAL_STORAGE等敏感权限适配Android 6.0以后版本的问题并非本文关注重点,在此不做赘述。

混淆代码

集成SDK并声明了权限后,就该考虑混淆的问题了,我们需要在project的proguard-rules.pro文件里添加以下代码:

  1. -keep class io.agora.**{*;}
  2. 复制代码

经过以上过程后,我们已经完成了声网Agora.io SDK的快速集成,迈出了走向连麦直播、在线抓娃娃、直播问答、远程狼人杀等风口的*步。在接下来的文章里,我将继续分享APP ID鉴权、Token鉴权、一对一视频聊天、创建群聊room、分屏、窗口切换和前后摄像头切换等内容。

鉴权

APP ID鉴权

所谓APP ID,就是在 Agora创建每个项目都有的一个唯一标识。App ID 可以明确你的项目及组织身份,并在 joinChannel 方法中作为参数,连接到 Agora 实时网络中,实现实时通信或直播功能。不同的App ID在Agora实时网络中的通话是完全隔离的;Agora 提供的频道信息、计费、管理服务也都是基于 App ID。

申请APP ID的操作很简便,只要在Agora官网https://dashboard.agora.io/projects右侧栏目的“项目”中点击“添加新项目”,只需输入项目名就可生成APP ID,全过程如下图所示:

找到,把“<#YOUR APP ID#>”替换为图中的马赛克里的字符串。

  1. <string name=“agora_app_id”><#YOUR APP ID#></string>
  2. 复制代码

以上就是APP ID鉴权的全过程。

尽管App ID鉴权在*大程度上方便了开发者使用 Agora 的服务。但App ID 鉴权的安全性不佳,一旦有别有用心的人非法获取了你的 App ID,他就可以在 Agora 提供的SDK中使用你的App ID。如果你的项目对安全性要求高,或者增加用户权限设置的话,建议采用Token鉴权。

Token鉴权

在通信和直播场景中存在着多个角色,而每种角色又对应着一些默认权限。比如在直播场景中,主播可以发布流、订阅流、邀请嘉宾;观众可以订阅流、申请连麦;管理员则可以踢人或禁言。

Token鉴权的步骤比APP ID鉴权稍微复杂一些,在上文项目列表中查看 App ID 的地方,启用该项目的 App Certificate:

首先,点击激活项目右上方的 编辑 按钮。

将你的 App Certificate 保存在服务器端,且对任何客户端均不可见。当项目的 App Certificate 被启用后,你必须使用 Token。例如: 在启用 App Certificate 前,你可以使用 App ID 加入频道。但启用了 App Certificate 后,你就必须使用 Token 加入频道。后台如何用App Certificate生成Token本文不做赘述。

初始化Agora

RtcEngine 类包含应用程序调用的主要方法,调用 RtcEngine 的接口*好在同一个线程进行,不建议在不同的线程同时调用。

目前 Agora Native SDK 只支持一个 RtcEngine 实例,每个应用程序仅创建一个 RtcEngine 对象 。 RtcEngine 类的所有接口函数,如无特殊说明,都是异步调用,对接口的调用建议在同一个线程进行。所有返回值为 int 型的 API,如无特殊说明,返回值 0 为调用成功,返回值小于 0 为调用失败。

IRtcEngineEventHandler接口类用于SDK向应用程序发送回调事件通知,应用程序通过继承该接口类的方法获取 SDK 的事件通知。

接口类的所有方法都有缺省(空)实现,应用程序可以根据需要只继承关心的事件。在回调方法中,应用程序不应该做耗时或者调用可能会引起阻塞的 API(如 SendMessage),否则可能影响 SDK 的运行。

  1. private RtcEngine mRtcEngine;
  2. /**
  3. * Tutorial Step 1
  4. * 初始化Agora,创建 RtcEngine 对象
  5. */
  6. private void initializeAgoraEngine() {
  7. try {
  8. mRtcEngine = RtcEngine.create(getBaseContext(), getString(R.string.agora_app_id), mRtcEventHandler);
  9. } catch (Exception e) {
  10. Log.e(LOG_TAG, Log.getStackTraceString(e));
  11. throw new RuntimeException(“Agora初始化失败了,检查一下是哪儿出错了\n” + Log.getStackTraceString(e));
  12. }
  13. }
  14. private final IRtcEngineEventHandler mRtcEventHandler = new IRtcEngineEventHandler() {
  15. @Override
  16. public void onFirstRemoteVideoDecoded(final int uid, int width, int height, int elapsed) {
  17. runOnUiThread(new Runnable() {
  18. @Override
  19. public void run() {
  20. //设置远端视频显示属性
  21. setupRemoteVideo(uid);
  22. }
  23. });
  24. }
  25. @Override
  26. public void onUserOffline(int uid, int reason) {
  27. runOnUiThread(new Runnable() {
  28. @Override
  29. public void run() {
  30. //其他用户离开当前频道回调
  31. onRemoteUserLeft();
  32. }
  33. });
  34. }
  35. @Override
  36. public void onUserMuteVideo(final int uid, final boolean muted) {
  37. runOnUiThread(new Runnable() {
  38. @Override
  39. public void run() {
  40. //其他用户已停发/已重发视频流回调
  41. onRemoteUserVideoMuted(uid, muted);
  42. }
  43. });
  44. }
  45. };
  46. private void onRemoteUserLeft() {
  47. FrameLayout container = (FrameLayout) findViewById(R.id.remote_video_view_container);
  48. container.removeAllViews();
  49. //文案可随意定制
  50. View tipMsg = findViewById(R.id.quick_tips_when_use_agora_sdk);
  51. tipMsg.setVisibility(View.VISIBLE);
  52. }
  53. private void onRemoteUserVideoMuted(int uid, boolean muted) {
  54. FrameLayout container = (FrameLayout) findViewById(R.id.remote_video_view_container);
  55. SurfaceView surfaceView = (SurfaceView) container.getChildAt(0);
  56. Object tag = surfaceView.getTag();
  57. if (tag != null && (Integer) tag == uid) {
  58. surfaceView.setVisibility(muted ? View.GONE : View.VISIBLE);
  59. }
  60. }
  61. 复制代码

打开视频模式

enableVideo()方法用于打开视频模式。可以在加入频道前或者通话中调用,在加入频道前调用,则自动开启视频模式,在通话中调用则由音频模式切换为视频模式。调用 disableVideo() 方法可关闭视频模式。

setVideoProfile()方法设置视频编码属性(Profile)。每个属性对应一套视频参数,如分辨率、帧率、码率等。 当设备的摄像头不支持指定的分辨率时,SDK 会自动选择一个合适的摄像头分辨率,但是编码分辨率仍然用 setVideoProfile() 指定的。

该方法仅设置编码器编出的码流属性,可能跟*终显示的属性不一致,例如编码码流分辨率为 640×480,码流的旋转属性为 90 度,则显示出来的分辨率为竖屏模式。

  1. /**
  2. * Tutorial Step 2
  3. * 打开视频模式,并设置本地视频属性
  4. */
  5. private void setupVideoProfile() {
  6. //打开视频模式
  7. mRtcEngine.enableVideo();
  8. //设置本地视频属性
  9. mRtcEngine.setVideoProfile(Constants.VIDEO_PROFILE_360P, false);
  10. }
  11. 复制代码

设置本地视频显示属性

setupLocalVideo( VideoCanvas local )方法用于设置本地视频显示信息。应用程序通过调用此接口绑定本地视频流的显示视窗(view),并设置视频显示模式。 在应用程序开发中,通常在初始化后调用该方法进行本地视频设置,然后再加入频道。退出频道后,绑定仍然有效,如果需要解除绑定,可以调用 setupLocalVideo(null) 。

  1. /**
  2. * Tutorial Step 3
  3. * 设置本地视频显示属性
  4. */
  5. private void setupLocalVideo() {
  6. FrameLayout container = (FrameLayout) findViewById(R.id.local_video_view_container);
  7. SurfaceView surfaceView = RtcEngine.CreateRendererView(getBaseContext());
  8. surfaceView.setZOrderMediaOverlay(true);
  9. container.addView(surfaceView);
  10. mRtcEngine.setupLocalVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_ADAPTIVE, 0));
  11. }
  12. 复制代码

加入一个频道

joinChannel(String token,String channelName,String optionalInfo,int optionalUid )方法让用户加入通话频道,在同一个频道内的用户可以互相通话,多个用户加入同一个频道,可以群聊。 使用不同 App ID 的应用程序是不能互通的。如果已在通话中,用户必须调用 leaveChannel() 退出当前通话,才能进入下一个频道。

  1. /**
  2. * Tutorial Step 4
  3. * 加入一个频道
  4. */
  5. private void joinChannel() {
  6. //如果不指定UID,Agroa将自动生成并分配一个UID
  7. mRtcEngine.joinChannel(null, “demoChannel1”, “Extra Optional Data”, 0);
  8. }
  9. 复制代码

设置远端视频显示属性

setupRemoteVideo( VideoCanvas remote)方法用于绑定远程用户和显示视图,即设定 uid 指定的用户用哪个视图显示。调用该接口时需要指定远程视频的 uid,一般可以在进频道前提前设置好。

如果应用程序不能事先知道对方的 uid,可以在 APP 收到 onUserJoined 事件时设置。如果启用了视频录制功能,视频录制服务会做为一个哑客户端加入频道,因此其他客户端也会收到它的 onUserJoined 事件,APP 不应给它绑定视图(因为它不会发送视频流),如果 APP 不能识别哑客户端,可以在 onFirstRemoteVideoDecoded 事件时再绑定视图。解除某个用户的绑定视图可以把 view 设置为空。退出频道后,SDK 会把远程用户的绑定关系清除掉。

  1. /**
  2. * Tutorial Step 5
  3. * 设置远端视频显示属性
  4. */
  5. private void setupRemoteVideo(int uid) {
  6. FrameLayout container = (FrameLayout) findViewById(R.id.remote_video_view_container);
  7. if (container.getChildCount() >= 1) {
  8. return;
  9. }
  10. SurfaceView surfaceView = RtcEngine.CreateRendererView(getBaseContext());
  11. container.addView(surfaceView);
  12. mRtcEngine.setupRemoteVideo(new VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_ADAPTIVE, uid));
  13. surfaceView.setTag(uid);
  14. //文案可随意定制
  15. View tipMsg = findViewById(R.id.quick_tips_when_use_agora_sdk);
  16. tipMsg.setVisibility(View.GONE);
  17. }
  18. 复制代码

离开当前频道

leaveChannel()方法用于离开频道,即挂断或退出通话。

当调用 joinChannel() API 方法后,必须调用 leaveChannel() 结束通话,否则无法开始下一次通话。 不管当前是否在通话中,都可以调用 leaveChannel(),没有副作用。该方法会把会话相关的所有资源释放掉。该方法是异步操作,调用返回时并没有真正退出频道。在真正退出频道后,SDK 会触发 onLeaveChannel 回调。

  1. /**
  2. * Tutorial Step 6
  3. * 离开当前频道
  4. */
  5. private void leaveChannel() {
  6. mRtcEngine.leaveChannel();
  7. }
  8. public void onEncCallClicked(View view) {
  9. finish();
  10. }
  11. @Override
  12. protected void onDestroy() {
  13. super.onDestroy();
  14. leaveChannel();
  15. RtcEngine.destroy();
  16. mRtcEngine = null;
  17. }
  18. 复制代码

管理摄像头

switchCamera()方法用于在前置/后置摄像头间切换。除此以外Agora还提供了一下管理摄像头的方法:例如setCameraTorchOn(boolean isOn)设置是否打开闪光灯、setCameraAutoFocusFaceModeEnabled(boolean enabled)设置是否开启人脸对焦功能等等。

  1. /**
  2. * Tutorial Step 7
  3. * 切换前置/后置摄像头
  4. */
  5. public void onSwitchCameraClicked(View view) {
  6. mRtcEngine.switchCamera();
  7. }
  8. 复制代码

将自己静音

muteLocalAudioStream(boolean muted)方法用于静音/取消静音。该方法可以允许/禁止往网络发送本地音频流。但该方法并没有禁用麦克风,不影响录音状态。

  1. /**
  2. * Tutorial Step 8
  3. * 将自己静音
  4. */
  5. public void onLocalAudioMuteClicked(View view) {
  6. ImageView iv = (ImageView) view;
  7. if (iv.isSelected()) {
  8. iv.setSelected(false);
  9. iv.clearColorFilter();
  10. } else {
  11. iv.setSelected(true);
  12. iv.setColorFilter(getResources().getColor(R.color.colorPrimary), PorterDuff.Mode.MULTIPLY);
  13. }
  14. mRtcEngine.muteLocalAudioStream(iv.isSelected());
  15. }
  16. 复制代码

暂停本地视频流

muteLocalVideoStream(boolean muted)方法用于暂停发送本地视频流,但该方法并没有禁用摄像头,不影响本地视频流获取。

  1. /**
  2. * Tutorial Step 9
  3. * 暂停本地视频流
  4. */
  5. public void onLocalVideoMuteClicked(View view) {
  6. ImageView iv = (ImageView) view;
  7. if (iv.isSelected()) {
  8. iv.setSelected(false);
  9. iv.clearColorFilter();
  10. } else {
  11. iv.setSelected(true);
  12. iv.setColorFilter(getResources().getColor(R.color.colorPrimary), PorterDuff.Mode.MULTIPLY);
  13. }
  14. mRtcEngine.muteLocalVideoStream(iv.isSelected());
  15. FrameLayout container = (FrameLayout) findViewById(R.id.local_video_view_container);
  16. SurfaceView surfaceView = (SurfaceView) container.getChildAt(0);
  17. surfaceView.setZOrderMediaOverlay(!iv.isSelected());
  18. surfaceView.setVisibility(iv.isSelected() ? View.GONE : View.VISIBLE);
  19. }
  20. 复制代码

运行效果

拿两部手机安装编译好的App,如果能看见两个自己,说明你成功了。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速