日期: 2021 年 4 月 25 日

Android两级导航菜单栏的实现

Android两级导航菜单栏的实现–FragmentTabHost结合ViewPager与Android 开源项目PagerSlidingTabStrip

简单总结一下,外层Tab用TabHost,类层Tab用Viepager+FramentStatePagerAdapter解决方案。

本篇将使用PagerSlidingTabStrip 和ViewPager实现子Tab导航菜单栏的页面滑动。当然,你也可以直接把PagerSlidingTabStrip 和ViewPager放到项目中当做主Tab导航菜单栏使用,只要思路掌握了,就可以随心所欲的灵活运用了。

 

先看一下效果图(二级Tab导航菜单栏可以实现滑动):

%title插图%num %title插图%num

重写fragment_message.xml

 

  1. <?xml version=“1.0” encoding=”utf-8″?>  
  2. <LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”  
  3.     android:layout_width=“match_parent”  
  4.     android:layout_height=“match_parent”  
  5.     android:orientation=“vertical” >  
  6.     <com.example.testbase.customer.PagerSlidingTabStrip  
  7.         android:id=“@+id/tabs”  
  8.         android:layout_width=“match_parent”  
  9.         android:layout_height=“40dp” />  
  10.     <android.support.v4.view.ViewPager  
  11.         android:id=“@+id/pager”  
  12.         android:layout_width=“match_parent”  
  13.         android:layout_height=“wrap_content”  
  14.         android:layout_below=“@+id/tabs” />  
  15. </LinearLayout>  

 

 

重写 FragmentMessage

 

 

  1. public class FragmentMessage extends Fragment {  
  2.     private SubFragment1 subFragment1;  
  3.     private SubFragment2 subFragment2;  
  4.     private SubFragment3 subFragment3;  
  5.     /** 
  6.      * PagerSlidingTabStrip的实例 
  7.      */  
  8.     private PagerSlidingTabStrip tabs;  
  9.     /** 
  10.      * 获取当前屏幕的密度 
  11.      */  
  12.     private DisplayMetrics dm;  
  13.     @Override  
  14.     public void onCreate(Bundle savedInstanceState) {// 在前面执行  
  15.         super.onCreate(savedInstanceState);  
  16.         // 获取参数  
  17.         Bundle bundle = getArguments();
  18.         if (null != bundle) {  
  19.             //  
  20.         }
  21.     }
  22.     @Override  
  23.     public View onCreateView(LayoutInflater inflater, ViewGroup container,  
  24.             Bundle savedInstanceState) {
  25.         T.showShort(getActivity(), “FragmentMessage==onCreateView”);  
  26.         View view = inflater.inflate(R.layout.fragment_message, null);  
  27.         initView(view);
  28.         return view;  
  29.     }
  30.     private void initView(View view) {  
  31.         dm = getResources().getDisplayMetrics();
  32.         ViewPager pager = (ViewPager) view.findViewById(R.id.pager);
  33.         tabs = (PagerSlidingTabStrip) view.findViewById(R.id.tabs);
  34.         pager.setAdapter(new MyPagerAdapter(getChildFragmentManager()));  
  35.         tabs.setViewPager(pager);
  36.         setTabsValue();
  37.     }
  38.     /** 
  39.      * 对PagerSlidingTabStrip的各项属性进行赋值。 
  40.      */  
  41.     private void setTabsValue() {  
  42.         // 设置Tab是自动填充满屏幕的  
  43.         tabs.setShouldExpand(true);  
  44.         // 设置Tab的分割线是透明的  
  45.         tabs.setDividerColor(Color.TRANSPARENT);
  46.         // tabs.setDividerColor(Color.BLACK);  
  47.         // 设置Tab底部线的高度  
  48.         tabs.setUnderlineHeight((int) TypedValue.applyDimension(  
  49.                 TypedValue.COMPLEX_UNIT_DIP, 1, dm));  
  50.         // 设置Tab Indicator的高度  
  51.         tabs.setIndicatorHeight((int) TypedValue.applyDimension(  
  52.                 TypedValue.COMPLEX_UNIT_DIP, 4, dm));// 4  
  53.         // 设置Tab标题文字的大小  
  54.         tabs.setTextSize((int) TypedValue.applyDimension(  
  55.                 TypedValue.COMPLEX_UNIT_SP, 16, dm)); // 16  
  56.         // 设置Tab Indicator的颜色  
  57.         tabs.setIndicatorColor(Color.parseColor(“#45c01a”));// #45c01a  
  58.         // 设置选中Tab文字的颜色 (这是我自定义的一个方法)  
  59.         tabs.setSelectedTextColor(Color.parseColor(“#45c01a”));// #45c01a  
  60.         // 取消点击Tab时的背景色  
  61.         tabs.setTabBackground(0);  
  62.     }
  63.     // FragmentPagerAdapter FragmentStatePagerAdapter //不能用FragmentPagerAdapter  
  64.     public class MyPagerAdapter extends FragmentStatePagerAdapter {  
  65.         public MyPagerAdapter(FragmentManager fm) {  
  66.             super(fm);  
  67.             // TODO Auto-generated constructor stub  
  68.         }
  69.         private final String[] titles = { “SubOne”, “SubTwo”, “SubThree” };  
  70.         @Override  
  71.         public CharSequence getPageTitle(int position) {  
  72.             return titles[position];  
  73.         }
  74.         @Override  
  75.         public int getCount() {  
  76.             return titles.length;  
  77.         }
  78.         @Override  
  79.         public Fragment getItem(int position) {  
  80.             switch (position) {  
  81.             case 0:  
  82.                 if (null == subFragment1) {  
  83.                     subFragment1 = new SubFragment1();  
  84.                 }
  85.                 return subFragment1;  
  86.             case 1:  
  87.                 if (null == subFragment2) {  
  88.                     subFragment2 = new SubFragment2();  
  89.                 }
  90.                 return subFragment2;  
  91.             case 2:  
  92.                 if (null == subFragment3) {  
  93.                     subFragment3 = new SubFragment3();  
  94.                 }
  95.                 subFragment1 = new SubFragment1();  
  96.                 return subFragment3;  
  97.             default:  
  98.                 return null;  
  99.             }
  100.         }
  101.     }
  102. }

 

 

再添加 SubFragment1(这里只给出一个,其它类似)

 

  1. public class SubFragment1 extends Fragment {  
  2.     @Override  
  3.     public View onCreateView(LayoutInflater inflater, ViewGroup container,  
  4.             Bundle savedInstanceState) {
  5.         T.showShort(getActivity(), “SubFragment1==onCreateView”);  
  6.         TextView tv = new TextView(getActivity());  
  7.         tv.setTextSize(25);  
  8.         tv.setBackgroundColor(Color.parseColor(“#FFA07A”));  
  9.         tv.setText(“SubFragment1”);  
  10.         tv.setGravity(Gravity.CENTER);
  11.         return tv;  
  12.     }
  13. }

感觉没什么可写了……当然,你会发现切换ViewPager的时候,它所管理的Fragment生命周期很有意思,具体需求具体解决,方案很多……

适配Android 华为等底部虚拟键

在app开发中有很多项目使用底部tab+ ViewPager + fragment 的框架,那么这个时候如果app安装在底部带有虚拟键的设备上的话,会产生设备底部的虚拟键遮挡app底部tab的情况,这个时候对app的外观和功能的使用都产生了很大的影响,下边我们对此情况进行适配。

1、首先我们进行工具类的封装,主要思路是addOnGlobalLayoutListener全局监听视图的变化(onGlobalLayoutListener是viewTreeObserver的内部类,当视图变化时onGlobalLayoutListener可以监听到),那么当视图的高度发生变化时,就对这个视图重新布局,使视图不被遮挡。

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;

import java.lang.reflect.Method;

public class NavigationBarUtil {
public static void initActivity(View content) {
new NavigationBarUtil(content);
}

private View mObserved;//被监听的视图
private int usableHeightView;//视图变化前的可用高度
private ViewGroup.LayoutParams layoutParams;

private NavigationBarUtil(View content) {
mObserved = content;
//给View添加全局的布局监听器监听视图的变化
mObserved.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
resetViewHeight();
}
});
layoutParams = mObserved.getLayoutParams();
}

/**
* 重置视图的高度,使不被底部虚拟键遮挡
*/
private void resetViewHeight() {
int usableHeightViewNow = CalculateAvailableHeight();
//比较布局变化前后的View的可用高度
if (usableHeightViewNow != usableHeightView) {
//如果两次高度不一致
//将当前的View的可用高度设置成View的实际高度
layoutParams.height = usableHeightViewNow;
mObserved.requestLayout();//请求重新布局
usableHeightView = usableHeightViewNow;
}
}

/**
* 计算试图高度
* @return
*/
private int CalculateAvailableHeight() {
Rect r = new Rect();
mObserved.getWindowVisibleDisplayFrame(r);
return (r.bottom – r.top);//如果不是沉浸状态栏,需要减去顶部高度
// return (r.bottom );//如果是沉浸状态栏
}

/**
* 判断底部是否有虚拟键
* @param context
* @return
*/
public static boolean hasNavigationBar(Context context) {
boolean hasNavigationBar = false;
Resources rs = context.getResources();
int id = rs.getIdentifier(“config_showNavigationBar”, “bool”, “android”);
if (id > 0) {
hasNavigationBar = rs.getBoolean(id);
}
try {
Class systemPropertiesClass = Class.forName(“android.os.SystemProperties”);
Method m = systemPropertiesClass.getMethod(“get”, String.class);
String navBarOverride = (String) m.invoke(systemPropertiesClass, “qemu.hw.mainkeys”);
if (“1”.equals(navBarOverride)) {
hasNavigationBar = false;
} else if (“0”.equals(navBarOverride)) {
hasNavigationBar = true;
}
} catch (Exception e) {
}
return hasNavigationBar;
}
}
2、在需要适配的activity 的 onCreate方法中的 super.onCreate(savedInstanceState)之后 调用
if(NavigationBarUtil.hasNavigationBar(this)){
NavigationBarUtil.initActivity(findViewById(android.R.id.content));
}
(推荐封装base 这样就可以直接继承base就可以)。

*后的效果就是app底部tab 在设备的虚拟键之上。

建设有中国特色的Android M&N(Android6.0和7.0新特性分析)

Android N已经发布有段时间,甚至马上都要发布Android 7.1,相信不少玩机爱好者已经刷入*新的Android N 7.1 Beta ROM 体验起来,即使你没有能够刷入的设备,通过模拟器来先行体验下,也不错。

不过对于Android的ROM来说,特别是国内的ROM厂家,估计就比较头大了,Merge代码估计要疯了。下面,我们就来看看Android N到现在给我们带来哪些新特性,以及这些新特性如何运用在自己的App中(这里包括了Android M&N,即Android 6.0和Android 7.0的新特性)。

运行时权限系统

这个应该是M&N系统更新以来*大的特性修改,也是各个ROM厂商*大的梦魇,因为这个功能,大部分的ROM厂商——都已经自己实现了!而现在,Google提出了统一的权限管理系统,真是非常符合Google的一贯作风——养肥了就杀!

这个权限管理系统并不是在ROM级别进行的控制,而是将权限申请工作交给了App开发者,由他们来把控App的使用权限,这样有利有弊,利是用户可以更加方便、自由的控制自己的隐私权限,这一点类似于iOS的权限控制,但Android并不是ROM直接托管的;而弊端,就是用户可能无法判断这个权限的作用而关闭这个权限,导致App无法正常工作。

所以,对于要适配Android M&N的App来说,权限的管理与交互是适配的重中之重,而且也是以前从来没有过的新概念——如何去设计权限申请的交互逻辑,考验产品经理的时候到了。

权限系统基本概念

现在Android把权限分为了敏感权限与非敏感权限,对于非敏感权限,开发者同样是在AndroidMainifest中进行权限申请,这些权限会在Android App安装的时候显示出来,与现在一样,而敏感权限,则会通过Dialog的方式在使用时弹出,如图所示。
图片描述

对于一个敏感权限来说,他有两条命,*条命是在*次显示该权限的时候,如果这个使用用户点击拒*,那么*条命就没了,当App第二次使用到这个权限的时候,界面会多出一个『不再询问』的选项,如果用户这里再继续拒*,或者勾选『不再询问』,那么第二条命就Game Over了,只能让用户去Setting中手动去管理这个权限了。

权限系统的交互

这段应该让产品经理来写。

不过作为开发者和用户,我也来写一点。
首先要知道的是,不管怎么交互,在Android M&N之后,用户都可以把App的某个敏感权限关闭,所以,权限系统的交互,实际上就是引导用户了解App所需要使用的权限,已经引导用户相信这些权限的使用是合理的(当然,基本都是不合理的)。

Google在他的官方文档中,给我们提供了四种权限交互的设计方式,我们可以来参考下:

  • Educate before asking

    这种就是类似于在闪屏页提示用户『我们需要哪些权限哦,这些权限是干嘛的哦,亲你一定要在弹出对话框的时候同意哦亲』。

  • Ask up-front

    这种方式简单粗暴,就是一上来就把需要申请的权限全部给用户,让他一个个去点,这种方式*简单,但也*野蛮,如果你的App敏感权限非常多,估计用户要崩溃的,可能会带来大量的流失。

  • Ask in context

    这种方式就是在用户使用到某个权限的时候,才去提醒用户权限的使用,这样的好处是,用户可以很清楚现在的权限作用,比如我现在要语音搜索,那么App提示需要麦克风权限,这很正常啊,就同意了。

  • Educate in context

    这种方式应该是第三种方式的补充,用于那些不是很明显的权限使用意图,例如我现在要拍照,你跟我申请定位权限,为啥呢,你需要解释下,可以记录照片的地理位置,这样我就知道了,否则我就会很奇怪,拍照要定位干嘛呢?

Chrome

作为Google的老牌App,Chrome在国外的浏览器市场上,已经占领了*对的优势,所以,原本在Android中自带的浏览器,就是Chrome浏览器,而不是国内这些ROM的什么250安全浏览器、企鹅浏览器等等。

那么Google在Android M&N中,对Chrome进行了一系列的优化,可惜,很多国内ROM都没法使用,但如果你的App的目标人群是一些可以使用Chrome的用户,那么这些新特性,就是你一定需要了解的。

Chrome App Links

这个就相当于在Web中使用Scheme来进行App的跳转功能一样,只不过这里Chrome默认就支持了,用户只需要搜索相关的关键词,就可以直接显示App,然后通过App Link直接启动App,如图所示。

图片描述

这个东西对用户来说应该是一件非常好、方便的事情,但由于国内这些浏览器的问题,不知道能否会兼容这个功能。

Android Pay

呵呵哒,算了不讲了,国内厂商都希望用自己的,这也就导致了被Apple独揽天下。什么时候这些ROM厂家能放弃这些利益,用Google提供的默认API支持,Android Pay就可以和Apple Pay分庭抗*了,毕竟现在在国外,Android Pay已经非常成熟了,而且新的指纹API的加入,让安全等级更上一层楼。

另外提一点,Android现在的安全等级已经非常高了,像指纹信息这些东西,都存储在单独的安全芯片上,而且,新的指纹API让开发者也可以非常方便的去使用指纹,不管是从用户还是开发者的角度来说,使用Android提供的原生安全方式,*对是好于自己实现的。

Direct Share

一个小的功能点,App可以实现Android提供的API,在分享的时候,直接将分享内容分享到更加精细的目标,例如联系人中的某个联系人,这样可以节省用户重新打开App的时间,也拓展了App之间的信息分享。

Notification && Notification Menu

通知真的是Android*iOS的一个巨大的筹码,可惜,很多国内ROM将通知改的一塌糊涂,很多关于Notification的新特性都无法使用,Notification带来的新的交互体验,也无法实现,可悲可叹。

通知栏样式修改

通知栏的样式修改,总体来说,让Notification的使用更加方便了,同时也更加好看,Google在2016的IO大会上也着重讲了,大家可以参考IO大会上的内容,这里放几种截图,大家随便体会下。

图片描述

图片描述

图片描述

通知栏快捷回复

在Android N上,Android对通知栏进行了进一步的优化,其中一个非常大的改变就是让用户可以在通知栏上直接对通知进行回复,这对于一些IM类的App来说,提供了更加友好的回复功能。

通知栏消息分组

如同描述的一样,系统可以支持对通知消息进行合并,相比于iOS的通知一条条展示来说,简直不能再赞了。

菜单选项定制

在Android N上,用户可以通过『Edit』按钮来定制自己的快捷菜单,是的,这也是很多厂商的ROM都已经有的功能,Google再一次养肥了杀了。

图片描述

这一功能的优势在于App可以定制自己的快捷功能,让使用频率高的一些功能,集成到系统的快捷菜单中。

Text Selection

Android M在系统级别,提供了对文字选择的API拓展,在以前的版本上,长按后,文字只提供剪贴、复制这些系统功能,而现在,系统为App提供了更多的访问接口,官方给出的例子比较复杂,如图所示。

图片描述

现在已经有App适配了这个功能了,如图所示。

图片描述

图片描述

可以看见,这一功能,对一些翻译类的App来说简直就是福音,同时,对于自己的App内部,通过这种方式,也可以产生很多比较好的交互体验。

系统级电话黑名单

难道国内的电信诈骗已经惊动了Google?不过这总归也算是一件好事,在Android N上,系统提供了黑名单的API,App可以调用,了解被拉入黑名单的电话信息。

Doze && App Standby

自从Android M开始,Google在Android中就加入了这样一个『省电』的机制,到Android N,这一机制被进一步完善,但不知道国内的ROM厂家会如何看待这个功能,或许会被阉割,或许省电阀值会被修改,目前还不得而知。但Google已经在官方文档中对这样一个功能App所需要的适配提供了解决方案:

https://developer.android.com/training/monitoring-device-state/doze-standby.html

App需要参照这个解决方案,来处理Doze和App Standby的功能与限制。

系统分屏多任务

Android N提供的一个比较大的特色功能,就是应用的分屏功能,当然,这依然是一个Google养肥了再杀的功能,很多ROM,例如LG、三星,其实很早之前就已经支持了,不过Google这次的实现更加完整、更加丰富。

图片描述

图片描述

那么分屏功能究竟有哪些使用场景呢?这依然是一个产品经理要解决的问题,对于开发者来说,我们在适配多设备分辨率、屏幕的同时,又增加了一个对系统分屏的适配工作,还好这些事情,Google都帮我们写好了文档。

VR

Android N的一个很大的特点就是对VR虚拟现实的支持,这东西现在越来越火,但在手机设备上,效果并不是很好,所以,Android N上对VR的支持,实际上是使用了一个新的跨平台图形计算库——Vulkan,所以说,如果一个手机支持VR,那么从某种意义上来说,这个手机的性能应该是很赞的!

以上,就是Android M&N的一些新特性,以及他们如何能够运用在我们自己的App中,希望大家能够多使用这些新特性来持续优化我们的App,为建设社会主义特色的App而奋斗!!!

深入理解Android 7.0系统权限更改相关文档

摘要:

Android 6.0之后的版本增加了运行时权限,应用程序在执行每个需要系统权限的功能时,需要添加权限请求代码(默认权限禁止),否则应用程序无法响应;Android 7.0在Android 6.0的基础上,对系统权限进一步更改,这次的权限更改包括三个方面:

  1. APP应用程序的私有文件不再向使用者放宽
  2. Intent组件传递file://URI的方式可能给接收器留下无法访问的路径,触发FileUriExposedException异常,推荐使用FileProvider
  3. DownloadManager不再按文件名分享私人存储的文件。旧版应用在访问COLUMN_LOCAL_FILENAME时可能出现无法访问的路径。面向 Android 7.0 或更高版本的应用在尝试访问 COLUMN_LOCAL_FILENAME 时会触发 SecurityException

简单的三句话,无法让TeachCourse真正理解Android 7.0系统权限更改的含义,如果不按照文档的方式去做,API 24开发的应用程序是否就用不了?

Android 7.0系统权限变更

一、深入理解FileProvider

FileProvider属于Android 7.0新增的一个类,该类位于v4包下,详情可见android.support.v4.content.FileProvider,使用方法类似与ContentProvider,简单概括为三个步骤,这里先以调用系统相机拍照并保存sdcard公共目录为例,演示使用过程:

  • 在资源文件夹res/xml下新建file_provider.xml文件,文件声明权限请求的路径,代码如下:
  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <paths xmlns:android=“http://schemas.android.com/apk/res/android”>
  3. <!–3、对应外部内存卡根目录:Environment.getExternalStorageDirectory()–>
  4. <external-path name=“ext_root” path=“/” />
  5. </paths>
  • AndroidManifest.xml添加组件provider相关信息,类似组件activity,指定resource属性引用上一步创建的xml文件(后面会详细介绍各个属性的用法),代码如下:
  1. <!– 定义FileProvider –>
  2. <provider
  3. android:name=“android.support.v4.content.FileProvider”
  4. android:authorities=“@string/install_apk_path”
  5. android:exported=“false”
  6. android:grantUriPermissions=“true”>
  7. <meta-data
  8. android:name=“android.support.FILE_PROVIDER_PATHS”
  9. android:resource=“@xml/file_provider” />
  10. </provider>
  • *后一步,Java代码申请权限,使用新增的方法getUriForFile()grantUriPermission(),代码如下(后面会详细介绍方法对应参数的使用):
  1. if (Build.VERSION.SDK_INT > 23) {
  2. /**Android 7.0以上的方式**/
  3. Uri contentUri = getUriForFile(this, getString(R.string.install_apk_path), file);
  4. grantUriPermission(getPackageName(), contentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
  5. intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
  6. }
  • 修改build.gradle文件compileSdkVersion大于或等于24,targetSdkVersion等于24,使用Android 7.0模拟器运行Demo,效果图:

Android 7.0系统权限更改

那么,我们已经了解Android 7.0系统权限申请的步骤,接下来说明每一个步骤需要注意的事项、相关方法参数的说明、属性的含义以及可以的申请权限目录(*后下载相关Demo)。

1.1 定义一个FileProvider

直接使用FileProvider本身或者它的子类,需要在AndroidManifest.xml文件中声明组件的相关属性,包括:

  • android:name,对应属性值:android.support.v4.content.FileProvider或者子类完整路径
  • android:authorities,对应属性值是一个常量,通常定义的方式packagename.fileprovider,例如:cn.teachcourse.fileprovider
  • android:exported,对应属性值是一个boolean变量,设置为false
  • android:grantUriPermissions,对应属性值也是一个boolean变量,设置为true,允许获得文件临时的访问权限
  1. <manifest>
  2. <application>
  3. <provider
  4. android:name=“android.support.v4.content.FileProvider”
  5. android:authorities=“com.mydomain.fileprovider”
  6. android:exported=“false”
  7. android:grantUriPermissions=“true”>
  8. </provider>
  9. </application>
  10. </manifest>

想要关联res/xml文件夹下创建的file_provider.xml文件,需要在<provider>标签内,添加<meta-data>子标签,设置<meta-data>标签的属性值,包括:

  • android:name,对应属性值是一个固定的系统常量android.support.FILE_PROVIDER_PATHS
  • android:resource,对应属性值指向我们的xml文件@xml/file_provider
  1. <provider
  2. android:name=“android.support.v4.content.FileProvider”
  3. android:authorities=“com.mydomain.fileprovider”
  4. android:exported=“false”
  5. android:grantUriPermissions=“true”>
  6. <meta-data
  7. android:name=“android.support.FILE_PROVIDER_PATHS”
  8. android:resource=“@xml/file_provider” />
  9. </provider>

1.2 指定授予临时访问权限的文件目录

上一步说明了怎么定义一个FileProvider,这一步主要说明怎么定义一个@xml/file_provider文件。Android Studio或Eclipse开发工具创建Android项目的时候默认不会创建res/xml文件夹,需要开发者手动创建,点击res文件夹新建目录,命名xml,如下图:

Android Studio新建xml目录

然后,在xml文件夹下新建一个xml文件,文件命名file_provider.xml,指定根标签为paths,如下图:

xml新建file_provider.xml

在xml文件中指定文件存储的区块和区块的相对路径,在<paths>根标签中添加<files-path>子标签(稍后详细列出所有子标签),设置子标签的属性值,包括:

  • name,是一个虚设的文件名(可以自由命名),对外可见路径的一部分,隐藏真实文件目录
  • path,是一个相对目录,相对于当前的子标签<files-path>根目录
  • <files-path>,表示内部内存卡根目录,对应根目录等价于Context.getFilesDir(),查看完整路径:
    /data/user/0/cn.teachcourse.demos/files
  • 代码如下:
  1. <paths xmlns:android=“http://schemas.android.com/apk/res/android”>
  2. <files-path name=“my_images” path=“images/”/>
  3. </paths>

<paths>根标签下可以添加的子标签也是有限的,参考官网的开发文档,除了上述的提到的<files-path>这个子标签外,还包括下面几个:

  1. <cache-path>,表示应用默认缓存根目录,对应根目录等价于getCacheDir(),查看完整路径:/data/user/0/cn.teachcourse.demos/cache
  2. <external-path>,表示外部内存卡根目录,对应根目录等价于
    Environment.getExternalStorageDirectory()
    查看完整路径:/storage/emulated/0
  3. <external-files-path>,表示外部内存卡根目录下的APP公共目录,对应根目录等价于
    Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)
    查看完整路径:
    /storage/emulated/0/Android/data/cn.teachcourse.demos/files/Download
  4. <external-cache-path>,表示外部内存卡根目录下的APP缓存目录,对应根目录等价于Context.getExternalCacheDir(),查看完整路径:
    /storage/emulated/0/Android/data/cn.teachcourse.demos/cache

*终,在file_provider.xml文件中,添加上述5种类型的临时访问权限的文件目录,代码如下:

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <paths xmlns:android=“http://schemas.android.com/apk/res/android”>
  3. <!–
  4. 1、name对应的属性值,开发者可以自由定义;
  5. 2、path对应的属性值,当前external-path标签下的相对路径
  6. 比如:/storage/emulated/0/92Recycle-release.apk
  7. sdcard路径:/storage/emulated/0(WriteToReadActivity.java:176)
  8. at cn.teachcourse.nougat.WriteToReadActivity.onClick(WriteToReadActivity.java:97)
  9. at android.view.View.performClick(View.java:5610)
  10. at android.view.View$PerformClick.run(View.java:22265)
  11. 相对路径:/
  12. –>
  13. <!–1、对应内部内存卡根目录:Context.getFileDir()–>
  14. <files-path
  15. name=“int_root”
  16. path=“/” />
  17. <!–2、对应应用默认缓存根目录:Context.getCacheDir()–>
  18. <cache-path
  19. name=“app_cache”
  20. path=“/” />
  21. <!–3、对应外部内存卡根目录:Environment.getExternalStorageDirectory()–>
  22. <external-path
  23. name=“ext_root”
  24. path=“pictures/” />
  25. <!–4、对应外部内存卡根目录下的APP公共目录:Context.getExternalFileDir(String)–>
  26. <external-files-path
  27. name=“ext_pub”
  28. path=“/” />
  29. <!–5、对应外部内存卡根目录下的APP缓存目录:Context.getExternalCacheDir()–>
  30. <external-cache-path
  31. name=“ext_cache”
  32. path=“/” />
  33. </paths>

1.3 生成指定文件的Content URI

Content URI方便与另一个APP应用程序共享同一个文件,共享的方式通过ContentResolver.openFileDescriptor获得一个ParcelFileDescriptor对象,读取文件内容。那么,如何生成一条完整的Content URI呢?TeachCourse总结后,概括为三个步骤,*步:明确上述5种类型中的哪一种,第二步:明确指定文件的完整路径(包括目录、文件名),第三步:调用getUriForFile()方法生成URI

  1. File imagePath = new File(Environment.getExternalStorageDirectory(), "download");
  2. File newFile = new File(imagePath, "default_image.jpg");
  3. Uri contentUri = getUriForFile(getContext(), "cn.teachcourse.fileprovider", newFile);

1.4 授予Content URI临时访问权限

上一步获得的Content URI,并没有获得指定文件的读写权限,想要获得文件的读写权限需要调用Context.grantUriPermission(package, Uri, mode_flags)方法,该方法向指定包名的应用程序申请获得读取或者写入文件的权限,参数说明如下:

  • package,指定应用程序的包名,Android Studio真正的包名指build.gradle声明的applicationId属性值;getPackageName()AndroidManifest.xml文件声明的package属性值,如果两者不一致,就不能提供getPackageName()获取包名,否则报错!
  • Uri,指定请求授予临时权限的URI,例如:contentUri
  • mode_flags,指定授予临时权限的类型,选择其中一个常量或两个:Intent.FLAG_GRANT_READ_URI_PERMISSIONIntent.FLAG_GRANT_WRITE_URI_PERMISSION

授予文件的临时读取或写入权限,如果不再需要了,TeachCourse该如何撤销授予呢?撤销权限有两种方式:*种:通过调用revokeUriPermission()撤销,第二种:重启系统后自动撤销

1.5 对外提供可访问的Content URI

有多种方式可以向客户端APP提供可访问文件的Content URI,其中一种常用的方式是通过发送Intent给需要启动的Activity,在重写的startActivityResult()方法中获取授予临时权限的Content URI或向用户提供可访问的接口来获取文件,后面的这种方式获取文件后转换成Content URI,以文章开头拍照的功能为例,TeachCourse想要在sdcard的公共目录pictures/查看已保存的照片,实现过程:

  • 请求授予访问公共目录的权限,代码如下:
  1. if (Build.VERSION.SDK_INT > 23) {
  2. /**Android 7.0以上的方式**/
  3. mStorageManager = this.getSystemService(StorageManager.class);
  4. StorageVolume storageVolume = mStorageManager.getPrimaryStorageVolume();
  5. Intent intent = storageVolume.createAccessIntent(Environment.DIRECTORY_PICTURES);
  6. startActivityForResult(intent, REQUEST_CODE_GRAINT_URI);
  7. }
  • 在重写的startActivityResult()方法中获取授予临时权限的Content URI,代码如下:
  1. @Override
  2. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  3. super.onActivityResult(requestCode, resultCode, data);
  4. switch (requestCode) {
  5. case REQUEST_CODE_GRAINT_URI:
  6. updateDirectoryEntries(data.getData());
  7. Log.d(TAG, "onActivityResult:Uri= "+data.getData());
  8. break;
  9. }
  10. }
  • 查询Environment.DIRECTORY_PICTURES目录,返回的Content URI包含的文件和文件类型相关信息,代码如下:
  1. private static final String[] DIRECTORY_SELECTION = new String[]{
  2. DocumentsContract.Document.COLUMN_DISPLAY_NAME,
  3. DocumentsContract.Document.COLUMN_MIME_TYPE,
  4. DocumentsContract.Document.COLUMN_DOCUMENT_ID,
  5. };
  6. @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  7. private void updateDirectoryEntries(Uri uri) {
  8. ContentResolver contentResolver = this.getContentResolver();
  9. Uri docUri = DocumentsContract.buildDocumentUriUsingTree(uri,
  10. DocumentsContract.getTreeDocumentId(uri));
  11. Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri,
  12. DocumentsContract.getTreeDocumentId(uri));
  13. try (Cursor docCursor = contentResolver
  14. .query(docUri, DIRECTORY_SELECTION, null, null, null)) {
  15. while (docCursor != null && docCursor.moveToNext()) {
  16. mPath_tv.setText(docCursor.getString(docCursor.getColumnIndex(
  17. DocumentsContract.Document.COLUMN_DISPLAY_NAME)));
  18. }
  19. }
  20. try (Cursor childCursor = contentResolver
  21. .query(childrenUri, DIRECTORY_SELECTION, null, null, null)) {
  22. while (childCursor != null && childCursor.moveToNext()) {
  23. String fileName = childCursor.getString(childCursor.getColumnIndex(
  24. DocumentsContract.Document.COLUMN_DISPLAY_NAME));
  25. String mimeType = childCursor.getString(childCursor.getColumnIndex(
  26. DocumentsContract.Document.COLUMN_MIME_TYPE));
  27. Log.e(TAG, "updateDirectoryEntries: "+fileName+"\n"+mimeType);
  28. }
  29. }
  30. }

运行Demo,控制台打印效果图:

Android 7.0访问sdcard

更多说明,可以参考Google提供的例子

二、深入理解DownloadManager

同样,为了方便理解DownloadManager的用法,首先以一个简单例子开始:从指定的url下载资源,然后显示下载资源的相关信息,运行Demo的效果图:

DownloadManager详解

Android 7.0系统权限更改的第三点,简单的说:通过访问COLUMN_LOCAL_FILENAME,在Android 7.0系统上可能无法获取Demo效果图fileName对应的文件路径,这时候可能触发异常SecurityException,打印的log信息,如下:

  1. Caused by: java.lang.SecurityException: COLUMN_LOCAL_FILENAME is deprecated; use ContentResolver.openFileDescriptor() instead
  2. at android.app.DownloadManager$CursorTranslator.getString(DownloadManager.java:1499)
  3. at cn.teachcourse.download.DownloadManagerActivity.query(DownloadManagerActivity.java:244)
  4. at cn.teachcourse.download.DownloadManagerActivity.access$100(DownloadManagerActivity.java:34)
  5. at cn.teachcourse.download.DownloadManagerActivity$1.onReceive(DownloadManagerActivity.java:186)
  6. at android.app.LoadedApk$ReceiverDispatcher$Args.run(LoadedApk.java:1122)
  7. at android.os.Handler.handleCallback(Handler.java:751)
  8. at android.os.Handler.dispatchMessage(Handler.java:95)
  9. at android.os.Looper.loop(Looper.java:154)
  10. at android.app.ActivityThread.main(ActivityThread.java:6077)
  11. at java.lang.reflect.Method.invoke(Native Method)
  12. at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865)
  13. at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755)

2.1 关于DownloadManager

DownloadManager是一个用于处理长时间HTTP请求的系统服务,客户端请求的URI可能是将要下载的指定的文件,处于后台的下载管理器将控制着下载的任务,并监测下载的状态,在下载失败或连接改变以及系统重启后尝试重新下载。

  • 如何初始化DownloadManager实例?首先调用getSystemService(String)方法,传入DOWNLOAD_SERVICE常量,来初始化DownloadManager实例,代码如下:
mDownloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
  • 如何配置请求参数?首先需要使用到内部类DownloadManager.Request,查看源码学习该类的各个方法的使用,TeachCourse简单总结:该类主要用于配置一条新下载任务相关内容,这些内容包括下载任务的保存路径,下载任务所处的网络状态(WiFi或流量状态)和下载任务通知栏显示样式等等,代码如下:
  1. /**
  2. * 设置请求下载的数据
  3. */
  4. private void initData() {
  5. //Request内部类配置新下载任务相关内容,比如:保存路径,WiFi或流量状态,下载通知栏样式
  6. request = new DownloadManager.Request(Uri.parse(mUrl + mFileName));
  7. request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, mFileName);
  8. request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE);
  9. request.setTitle("正在下载应用程序");
  10. request.setDescription("92回收,就爱回收APP");
  11. request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
  12. }
  • 如何开启下载任务?下载任务参数配置完成后,就可以开启后台服务下载,同一个DownloadManager实例,可以开启多个下载任务,需要上一步中配置多条URI,每个下载任务分配唯一的id,代码如下:
  1. /**
  2. * 下载任务的唯一标识ID,用于查询下载文件的相关信息
  3. */
  4. private void start() {
  5. mDownloadUniqueId = mDownloadManager.enqueue(request);
  6. mDownloadManager_btn.setText("正在下载。。。");
  7. mDownloadManager_btn.setClickable(false);
  8. }
  • DownloadManager通过两种状态的广播,*种:任务下载完成后发送,广播拦截器过滤action是DownloadManager.ACTION_DOWNLOAD_COMPLETE(关于广播的知识,不懂的可以参考TeachCourse博客另外的几篇文章);第二种:点击通知栏进度条后发送,广播拦截器过滤action是DownloadManager.ACTION_NOTIFICATION_CLICKED,代码如下:
  1. /**
  2. * 注册下载完成广播接收器,还可以注册其它监听器,比如:DownloadManager.ACTION_NOTIFICATION_CLICKED
  3. */
  4. private void registerReceiverCompleted() {
  5. IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
  6. registerReceiver(mBroadcastReceiver, intentFilter);
  7. }
  8. /**
  9. * 接收下载完成广播
  10. */
  11. private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
  12. @Override
  13. public void onReceive(Context context, Intent intent) {
  14. long reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
  15. if (mDownloadUniqueId == reference) {
  16. query(reference);
  17. mShowInformation_tv.setText(information);
  18. mDownloadManager_btn.setText("点击下载");
  19. mDownloadManager_btn.setClickable(true);
  20. }
  21. }
  22. };
  • 如何查询下载任务的相关信息?首先需要使用到内部类DownloadManager.Query,查看源码学习该类各个方法的使用,TeachCourse简单总结:该类正如文章开头样式的例子,通过分配的id查询下载任务相关的信息,这些信息包括文件类型、文件的Uri和文件的长度等,代码如下:
  1. /**
  2. * 查询下载任务相关的信息,比如:文件名、文件大小、文件类型等
  3. *
  4. * @param reference
  5. */
  6. private void query(long reference) {
  7. DownloadManager.Query query = new DownloadManager.Query();
  8. /**指定查询条件**/
  9. query.setFilterById(reference);
  10. /**查询正在等待、运行、暂停、成功、失败状态的下载任务**/
  11. query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
  12. Cursor cursor = mDownloadManager.query(query);
  13. if (cursor.moveToFirst()) {
  14. int fileId = cursor.getColumnIndex(DownloadManager.COLUMN_ID);
  15. int fileTitleId = cursor.getColumnIndex(DownloadManager.COLUMN_TITLE);
  16. int fileDescriptionId = cursor.getColumnIndex(DownloadManager.COLUMN_DESCRIPTION);
  17. int fileTypeId = cursor.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE);
  18. int fileLengthId = cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES);
  19. int fileUriId = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);
  20. /**过时的方式:DownloadManager.COLUMN_LOCAL_FILENAME**/
  21. int fileNameId = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);
  22. int statusCodeId = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
  23. int statusReasonId = cursor.getColumnIndex(DownloadManager.COLUMN_REASON);
  24. int downloadSizeId = cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
  25. int lastModifiedTimeId = cursor.getColumnIndex(DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP);
  26. int mediaUriId = cursor.getColumnIndex(DownloadManager.COLUMN_MEDIAPROVIDER_URI);
  27. String id = cursor.getString(fileId);
  28. String fileTitle = cursor.getString(fileTitleId);
  29. String description = cursor.getString(fileDescriptionId);
  30. String type = cursor.getString(fileTypeId);
  31. String length = cursor.getString(fileLengthId);
  32. String statusCode = cursor.getString(statusCodeId);
  33. String statusReason = cursor.getString(statusReasonId);
  34. String downloadSize = cursor.getString(downloadSizeId);
  35. String modifiedTime = cursor.getString(lastModifiedTimeId);
  36. String mediaUri = cursor.getString(mediaUriId);
  37. String fileUri = cursor.getString(fileUriId);
  38. String fileName = null;
  39. if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
  40. openFile(type, Uri.parse(fileUri));
  41. fileName = Uri.parse(fileUri).getPath();
  42. } else {
  43. /**Android 7.0以上的方式:请求获取写入权限,这一步报错**/
  44. fileName = cursor.getString(fileNameId);
  45. openFile(type, Uri.parse(fileUri));
  46. }
  47. /**清空StringBuffer存储的数据**/
  48. mStringBuffer.delete(0, mStringBuffer.length());
  49. mStringBuffer.append("id:" + id + "\n");
  50. mStringBuffer.append("fileTitle:" + fileTitle + "\n");
  51. mStringBuffer.append("description:" + description + "\n");
  52. mStringBuffer.append("type:" + type + "\n");
  53. mStringBuffer.append("length:" + length + "\n");
  54. mStringBuffer.append("fileName:" + fileName + "\n");
  55. mStringBuffer.append("fileUri:" + fileUri + "\n");
  56. mStringBuffer.append("statusCode:" + statusCode + "\n");
  57. mStringBuffer.append("statusReason:" + statusReason + "\n");
  58. mStringBuffer.append("downloadSize:" + downloadSize + "\n");
  59. mStringBuffer.append("modifiedTime:" + modifiedTime + "\n");
  60. mStringBuffer.append("mediaUri:" + mediaUri + "\n");
  61. information = mStringBuffer.toString();
  62. }
  63. cursor.close();
  64. }
  • 代码加入判断语句,如果非Android 7.0系统继续访问COLUMN_LOCAL_FILENAME获得文件存储的*对路径(上面中间部分代码),openFile()方法代码如下:
  1. /**
  2. * 根据文件的类型,指定可以打开的应用程序
  3. *
  4. * @param type
  5. * @param uri
  6. */
  7. private void openFile(String type, Uri uri) {
  8. if (type.contains("image/")) {
  9. try {
  10. ParcelFileDescriptor descriptor = getContentResolver().openFileDescriptor(uri, "r");
  11. FileDescriptor fileDescriptor = descriptor.getFileDescriptor();
  12. Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
  13. mShowPic_iv.setVisibility(View.VISIBLE);
  14. mShowPic_iv.setImageBitmap(bitmap);
  15. } catch (FileNotFoundException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }

现在,我们已经掌握了DownloadManager怎么实例化、怎么配置下载任务、怎么开启后台服务以及如何查询任务相关信息,想要实现一个应用程序版本更新就变得很简单,实现多任务下载也不是难事,完整源码参考文章后台通过的Demo。

三、关于ParcelFileDescriptor和FileDescriptor总结

官网的文档推荐我们使用ContentResolver.openFileDescriptor()方法,获得一个ParcelFileDescriptor对象,再通过getFileDescriptor()方法返回一个FileDescriptor,它们之间的关系参考上面的代码。

FileDescriptor通常被称为文件描述符,可以理解成本地的一个文件,通过流的方式读取文件内容以及通过流的方式写入数据到文件,这里是读取或写入数据到FileDescriptor中,假如我们的Uri表示的是一个txt文件,获取FileDescriptor对象后,通过下面的代码读取txt文件的内容:

FileInputStream fis = new FileInputStream(fd);

同理,写入数据到txt文件,代码如下:

  1. FileOutputStream out = new FileOutputStream(fd);
  2. out.write('写入数据到txt文件中');
  3. out.close();

获取到输入流或输出流后,剩下的就是关于流的操作了,划分为:文件字节流、文件字符流、缓冲流、数组流等

3.1 改写上面的例子

openFile()方法使用封装好的decodeFileDescriptor(),查看BitmapFactory.decodeFileDescriptor()相关源码,学习如何读取文件描述符中的内容,这里TeachCourse根据读取流的方式,改写如下:

  1. ...
  2. Bitmap bitmap = BitmapFactory.decodeStream(getStreamByFileDescriptor(fileDescriptor));
  3. ...
  4. /**
  5. * 通过流的方式读取内容
  6. *
  7. * @param fileDescriptor
  8. * @return
  9. */
  10. private InputStream getStreamByFileDescriptor(FileDescriptor fileDescriptor) {
  11. return new FileInputStream(fileDescriptor);
  12. }

于是,可以对FileDescriptor进行简单的封装成writeData()readData(),代码如下:

  1. /**往FileDescriptor中写入数据
  2. * @param fileDescriptor
  3. * @param content
  4. */
  5. private void writeData(FileDescriptor fileDescriptor, String content) {
  6. FileOutputStream fos = new FileOutputStream(fileDescriptor);
  7. try {
  8. fos.write(content.getBytes());
  9. } catch (IOException e) {
  10. e.printStackTrace();
  11. } finally {
  12. try {
  13. fos.close();
  14. } catch (IOException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. }
  19. /**从FileDescriptor中读取数据
  20. * @param fileDescriptor
  21. * @return
  22. */
  23. private String readData(FileDescriptor fileDescriptor) {
  24. FileInputStream fis = new FileInputStream(fileDescriptor);
  25. byte[] b = new byte[1024];
  26. int read;
  27. String content=null;
  28. try {
  29. while ((read = fis.read(b)) != -1) {
  30. content = new String(b, 0, read);
  31. }
  32. } catch (IOException e) {
  33. e.printStackTrace();
  34. } finally {
  35. try {
  36. fis.close();
  37. } catch (IOException e) {
  38. e.printStackTrace();
  39. }
  40. }
  41. return content;
  42. }

总结:

Android 7.0系统的权限更改,包括三个方面,文章从第二方面开始讲解,着重介绍了FileProviderDownloadManager两个类的使用,花了好长时间整理、测试和编辑,如果对你有帮忙,别忘了收藏和分享咯!

  1. FileProvider源码路径:nougat/WriteToReadActivity.java
  2. DownloadManager源码路径:download/DownloadActivity.java
  3. Demo源码
  4. 参考资料:https://developer.android.google.cn/about/versions/nougat/android-7.0-changes.html

android 7.0文件读写权限配置

在gradle中指定applicationId,这里也会跟着变了AdroidManifest.xml

android:authorities=”${applicationId}.fileProvider”
<provider
android:name=”android.support.v4.content.FileProvider”
android:authorities=”${applicationId}.FileProvider”
android:exported=”false”
android:grantUriPermissions=”true”>
<meta-data
android:name=”android.support.FILE_PROVIDER_PATHS”
android:resource=”@xml/rc_file_path” />
res/xml/rc_file_path.xml 文件的名字没有要求,只要放在xml文件夹下面就可以了
<?xml version=”1.0″ encoding=”utf-8″?>
<paths>

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

</paths>
paths这个元素内可以包含以下一个或多个

<files-path name=”name” path=”path” />对应内部内存卡根目录:Context.getFileDir() + /path/

<cache-path name=”name” path=”path” />对应应用默认缓存根目录,物理路径相当于Context.getCacheDir() + /path/

<external-path name=”name” path=”path” />对应外部内存卡根目录,物理路径相当于Environment.getExternalStorageDirectory() + /path/

<external-files-path name=”name” path=”path” />对应外部内存卡根目录下的APP公共目录,物理路径相当于Context.getExternalFilesDir(String) + /path/

<external-cache-path name=”name” path=”path” />对应外部内存卡根目录下的APP缓存目录,物理路径相当于Context.getExternalCacheDir() + /path/

拍照:

//创建图片存放file
File imgFile = new File(“照片存放目录”);
Uri uri;
//根据当前系统版本决定使用哪个api,N是Android7.0的代号
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//*个参数是上下文,第二个参数来自清单文件,必须完全一样,第三个参数为上面创建的照片file
uri = FileProvider.getUriForFile(this, “${applicationId}.FileProvider”, imgFile);
} else {
//Android7.0还用原先的api
uri = Uri.fromFile(imgFile);
}
//设置拍照后保存目录
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
应用安装
//创建安装包file
File apkFile = new File(path);
Uri uri;
//根据当前系统版本决定使用哪个api,N是Android7.0的代号
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(context, “{applicationId}.FileProvider”, apkFile);
} else {
//Android7.0还用原先的api
uri = Uri.fromFile(apkFile);
}
//当前代码在Activity里则下面这句可省略
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
//授权Intent读取URI和写URI的权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
intent.setDataAndType(uri,”application/vnd.android.package-archive”);
context.startActivity(intent);
 

Android 7.0 文件权限

1 FileProvider方式

这是谷歌官方推荐的解决方案。即使用FileProvider来生成一个content://格式的URI。具体实现方式如下:

  1. manifest声明
    在manifest中声明一个provider。name(即类名)为android.support.v4.content.FileProvider。
  1. <manifest>
  2. <application>
  3. <provider
  4. android:name=“android.support.v4.content.FileProvider”
  5. android:authorities=“com.mydomain.fileprovider”
  6. android:exported=“false”
  7. android:grantUriPermissions=“true”>
  8. <meta-data
  9. android:name=“android.support.FILE_PROVIDER_PATHS”
  10. android:resource=“@xml/file_paths” />
  11. </provider>
  12. </application>
  13. </manifest>

其中authorities可以自定义。为了避免和其它app冲突,*好带上自己app的包名。file_paths.xml中编写该Provider对外提供文件的目录。文件放置在res/xml/下。
2.编写file_paths.xml
文件格式如下:

  1. <paths xmlns:android=“http://schemas.android.com/apk/res/android”>
  2. <files-path name=“my_images” path=“images/”/>
  3. </paths>

内部的element可以是files-pathcache-pathexternal-pathexternal-files-pathexternal-cache-path,分别对应Context.getFilesDir(),Context.getCacheDir(),Environment.getExternalStorageDirectory(),Context.getExternalFilesDir(),Context.getExternalCacheDir()等几个方法。后来翻看源码发现还有一个没有写进文档的,但是也可以使用的element,是root-path,直接对应文件系统根目录。不过既然没有写进文档中,其实还是有将来移除的可能的。使用的话需要注意一下风险。

3.在Java代码当中使用
以分享一个图片为例:

  1. File file = …; //要分享的图片文件
  2. Uri uri = FileProvider.getUriForFile(context, “com.mydomain.fileprovider”, file); //第二个参数是manifest中定义的`authorities`
  3. Intent intent = new Intent(Intent.ACTION_SEND);
  4. intent.setType(“image/*”);
  5. intent.putExtra(Intent.EXTRA_TITLE, title);
  6. intent.putExtra(Intent.EXTRA_TEXT, text);
  7. intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //这一步很重要。给目标应用一个临时的授权。
  8. startActivity(intent); //或者其它*终处理方式

2 VmPolicy方式

以上方法固然是推荐使用的,正确的方法。但是我在实际开发中遇到这样的问题。某些应用(此处点名新浪微博)根本无法理解一个指向文件的content://格式的URI。新浪微博接收到这类URI之后,无法加载图片,并会在点击发送微博时崩溃。
另一方面,新浪微博对权限管理的处理采取了一种比较流氓的方式。它会在启动时申请文件读写权限,而如果拒*该权限的话,居然就直接退出了。我反正是不信什么需要文件权限来放缓存放数据的说辞。放缓存放数据有着一堆不需要权限的目录可用。但是这样一来,我们其实是不需要担心传递一个file://格式URI过去而对方没有权限的。
话说回来,如何解决这一问题呢?我在调研的时候观察到严格模式的一个方法:StrictMode.VmPolicy.Builder.detectFileUriExposure()。顾名思义,调用这个方法就会检测FileUriExposure这件事。这个方法其实从API18就有了,是不是有可能在API24变成了默认选项呢?
在Application.onCreate加入如下代码,置入一个不设防的VmPolicy:

  1. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
  2. StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
  3. StrictMode.setVmPolicy(builder.build());
  4. }

再用旧的方式直接把file://格式的URI发送出去。居然成功了,没有再抛出FileUriExposedException。

3 小结

*终我采取的综合方案是,先使用PackageManager.checkPermission检测对方的app有没有取得文件读写权限。如果有的话,给对方发送file://格式URI。如果没有的话,给对方发送FileProvider生成的URI并临时授权。
原本一个有标准解决方案的问题,因为某些应用不遵循规范而需要做更多的调研和workaround。实在是麻烦。希望可以帮助到遇到同样问题的人。

iPhone 13 有新消息了!

iPhone 13 或将全系支持激光雷达扫描仪,1 月 5 日,DigiTimes 消息,苹果计划在 2021 年发布的所有 iPhone 13 型号中增加激光雷达扫描仪功能。

%title插图%num

苹果在 2020 年 3 月推出的 iPad Pro 上首次应用激光雷达扫描仪。随后,在去年发布的 iPhone 12 Pro 和 iPhone 12 Pro Max 上配备了激光雷达扫描仪。

根据外媒报道2021年苹果将会在秋季发布会发布四款新iPhone,暂称为iPhone 13。

iPhone 13尺寸和iPhone 12系列保持一致,分别为:5.4英寸、6.1英寸、6.7英寸。

%title插图%num

屏幕屏幕为:

iPhone 13 mini:5.4英寸 60Hz LTPS OLED

iPhone 13 :6.1英寸 60Hz LTPS OLED

iPhone 13 Pro:6.1英寸 120Hz LTPO OLED

iPhone 13 Pro Max:6.7英寸 120Hz LTPO OLED

%title插图%num

LTPO 屏幕根据显示内容类型,在1Hz和120Hz之间后台切换,可更节省功耗,同时色准也不会丢失。iPhone 13依旧采用了刘海屏,也有可能刘海尺寸会减小一点,看到这个消息我的12感觉就没有香过一次!

iPhone12基带确认,果粉放心

在iPhone12发布的时候都没有提到iPhone12到底用的是什么基带,我之前在粉丝留言下方说了是高通基带,我当时说可能部分手机是因特尔部分是高通,今天有人在社交媒体发布了iPhone12的基带从中可以看出本次iPhone12采用的是高通的X55基带,(全部都这基带)和之前传闻所说一致,这样信号就比之前好很多。

%title插图%num

大家都知道上一代iPhone11基带是因特尔,我是亲子体验了,在电梯里朋友的安卓手机还有网络,我的就无服务了!真的很郁闷。不过这次改为高通相信信号会比之前好很多。

%title插图%num

高通基带大家都知道,信号方面非常稳定,相比其他芯片,高通算是很厉害的了,好了今天就分享到这里,看完这个文章大家就彻底知道iPhone12基带了吧,之前大家都知道用拨号方式也能查询出手机什么基带,但是目前iPhone12还不能激活没办法通过拨号来查看基带,这都是网上拆解大神进行拍摄。

iPhone的解锁、越狱、激活、固件等等是什么意思,有什么分别?

一、有锁和无锁
比如现在有一部iPhone,3G iPhone,有锁版或无锁版,有锁版就是加了网络锁,也就是绑定了运营商,比如美版的AT&T,英国的O2。无锁版也叫官方解锁版,比如港行里的无锁版(香港另有和记的“3”定制版iPhone)。

二、越狱
越狱对有锁和无锁的都适用。越狱是指通过改变一些程序使得iPhone的功能得到加强。
不论有锁版还是无锁版,如果只是想用App Store里的正版小软件,那么就不用越狱;如果想安装更多的好玩的非App Store里的软件,或者是说“盗版软件”,那么就得越狱。

三、解锁
解锁只适用于有锁版。如果不需要换运营商,当然不用解锁。但比如,对流到中国大陆的美版水货来说,不解锁那也不可能用AT&T,只能解锁。
解锁有两种方法,硬解或软解,软解的方法没放出来之前,都是硬解,*常见的硬解便是使用卡贴。
通常说的破解,当是“解锁+越狱”的合称,对很多iPhone用户(尤其是中国大陆的美版iPhone使用者)来说,这两个都是需要(当然还是那句话,越狱不是必须,只是更好玩)的,所以很多破解方式就将解锁和越狱的方法放在一块,提供给大家。

四、解锁和越狱谁先谁后
这一条网上似乎没有统一的说法,但就我看到的一些资料来分析,越狱虽然不是必须的,但对于需要解锁的iPhone来说,还是得先越狱,才能解锁,因为解锁需要使用到越狱后的一些程序才可以,尤其是软解。
但可能很多破解方式并不严格区分,因此众人对两者的认知不是那么泾渭分明。

五、激活在哪一步
任何版iPhone在正常使用通话功能之前,都必须走激活这一步,我们可以分下面三种情况来看。
有锁版——不解锁——使用前可以先激活再越狱,也可以先越狱再激活,不越狱就直接激活。
有锁版——解锁——因为按我的观点是解锁前先越狱,所以可以先激活再破解,或破解后再激活(其中中国大陆的美版用户先破解后当是不用再激活的,激活应该已经加入在破解的方法中了)
无锁版——不论越狱与否,可以先激活再越狱,也可以先越狱后激活,不越狱就直接激活。

其中,对于中国大陆的美版用户来说,除非有激活的好办法,否则都是得越狱才能正常使用电话功能。也意味着,大陆的美版用户,如果没有激活办法,就必须先越狱了,当然,还得解锁,也就是必须“破解”了。

这个激活在卡贴盛行时也是个麻烦事。有需要SIM卡,有不需要SIM的,我不是很清楚。

六、固件、固件升级
先说固件升级,类似于电脑的操作系统更新,比如大补丁(例如Windows XP的SP2)或更新程序(小补丁),或版本更新(比如XP到Vista)。“恢复”就是格式化重装,“更新”就是在保留原有程序的基础上进行升级。应该是“更新”比较慢,但“恢复”会造成数据在没备份的情况下损失。
固件,大概来说就相当于电脑的操作系统或功能更高级BIOS。
固件升级(恢复或更新)与越狱与否不冲突。但对于已经解锁后的有锁版iPhone来说,不论越狱与否,不要随便升级固件,否则会造成iPhone被锁,也就相当于说固件升级会使得之前的解锁失效并且无法使用(因为破解方法总不会比固件版本更新更快,虽然黑客很厉害,但是Apple的设计师难道是吃素的呀)。至于这个无法使用的具体状况,当有很多种。

不解锁的有锁版(仅指正常使用的,比如使用AT&T网络iPhone的美国人民),以及无锁版,固件升级当是无所谓的,只是如果之前已越狱,已安装的非正版的App Store里的小软件,可能无法继续使用(当然有的小软件却可以使用,这应该和软件的编程与Apple官方的检测限制有关)。

另外,固件升级后,应当是需要重新激活。不过刚才说了,对于已解锁的有锁版iPhone,如果贸然升级固件,有的会造成iPhone变成“砖头”,不用考虑说破不破解了,想激活都没方法。
在破解不得当或破解失败的情况后,有的也会变成“砖头”,按我的理解,“砖头”就是连iPod功能都不好使滴。

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