zoukankan      html  css  js  c++  java
  • android学习笔记三

    一、Android数据存储,参考http://www.cnblogs.com/ITtangtang/p/3920916.html

    ①最熟悉的是文件存储的方式,分两种:

      1、保存在手机的文件目录中(也可以是缓存目录中,使用了Context对象),

      2、保存在sd卡上(使用了Environment对象获取SD卡信息,参考:http://www.cnblogs.com/mengdd/p/3742623.html)。

    保存文件时的选择:不太重要的可以存在缓存目录中(可以直接被删除);较重要的存在一般目录中;最重要的可以存在SD卡上。

    ②使用SharedPreferences保存数据同样是保存文件形式,但是是xml格式的文件。

    Android解析xml文件的方式:Android提供了多种解析器,但是推荐使用pull解析器。参考:http://blog.csdn.net/liuhe688/article/details/6415593.

    ③使用SQlite数据库存储数据,和mysql等相比的特点是:在客户端,不用安装服务器。Android中通过两个对象来进行操作:SQLiteOpenHelper,SQLiteDatabase

    参考:http://www.cnblogs.com/Excellent/archive/2011/11/19/2254888.html

    ④使用内容提供者:这是应用之间传递数据的方式,一个应用作为提供者,另一个进行查询(这也是使用SQLite数据库,但是是对另一个用的数据库进行CURD操作)。

      1、提供者将数据操作的类(dao类)继承ContentProviders即可将其暴露,并通过UriMatcher对象设置访问路径且只有uri匹配时才进行相应操作。

        注意:此时的dao类需要实现ContentProviders的抽象方法来进行CURD操作;内容提供者可以设置权限进行控制访问。

      2、在配置文件中进行配置(包含其本身的声明和权限的配置)

      1 import android.content.ContentProvider;
      2 import android.content.ContentUris;
      3 import android.content.ContentValues;
      4 import android.content.UriMatcher;
      5 import android.database.Cursor;
      6 import android.database.sqlite.SQLiteDatabase;
      7 import android.net.Uri;
      8 
      9 public class PersonContentProvider extends ContentProvider {
     10     
     11     private static final String AUTHORITY = "com.itheima28.sqlitedemo.providers.PersonContentProvider";
     12     private static final int PRESON_INSERT_CODE = 0;    // 操作person表添加的操作的uri匹配码
     13     private static final int PERSON_DELETE_CODE = 1;
     14     private static final int PERSON_UPDATE_CODE = 2;
     15     private static final int PERSON_QUERY_ALL_CODE = 3;
     16     private static final int PERSON_QUERY_ITEM_CODE = 4;
     17     
     18     private static UriMatcher uriMatcher;
     19     private PersonSQLiteOpenHelper mOpenHelper;        // person表的数据库帮助对象
     20     
     21     static {
     22         uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     23         // 添加一些uri(类似于分机号)
     24         
     25         // content://com.itheima28.sqlitedemo.providers.PersonContentProvider/person/insert
     26         uriMatcher.addURI(AUTHORITY, "person/insert", PRESON_INSERT_CODE);
     27         
     28         // content://com.itheima28.sqlitedemo.providers.PersonContentProvider/person/delete
     29         uriMatcher.addURI(AUTHORITY, "person/delete", PERSON_DELETE_CODE);
     30 
     31         // content://com.itheima28.sqlitedemo.providers.PersonContentProvider/person/update
     32         uriMatcher.addURI(AUTHORITY, "person/update", PERSON_UPDATE_CODE);
     33         
     34         // content://com.itheima28.sqlitedemo.providers.PersonContentProvider/person/queryAll
     35         uriMatcher.addURI(AUTHORITY, "person/queryAll", PERSON_QUERY_ALL_CODE);
     36         
     37         // content://com.itheima28.sqlitedemo.providers.PersonContentProvider/person/query/#
     38         uriMatcher.addURI(AUTHORITY, "person/query/#", PERSON_QUERY_ITEM_CODE);
     39     }
     40 
     41     //这里可以通过该方法进行初始化,而不用通过构造函数
     42     @Override
     43     public boolean onCreate() {
     44         mOpenHelper = new PersonSQLiteOpenHelper(getContext());
     45         return true;
     46     }
     47 
     48     //这个方法是由系统调用的,用于说明返回值的MIME类型
     49     @Override
     50     public String getType(Uri uri) {
     51         switch (uriMatcher.match(uri)) {
     52         case PERSON_QUERY_ALL_CODE: // 返回多条的MIME-type
     53             return "vnd.android.cursor.dir/person";
     54         case PERSON_QUERY_ITEM_CODE: // 返回单条的MIME-TYPE
     55             return "vnd.android.cursor.item/person";
     56         default:
     57             break;
     58         }
     59         return null;
     60     }
     61 
     62     @Override
     63     public Cursor query(Uri uri, String[] projection, String selection,
     64             String[] selectionArgs, String sortOrder) {
     65         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
     66         switch (uriMatcher.match(uri)) {
     67         case PERSON_QUERY_ALL_CODE:  // 查询所有人的uri
     68             if(db.isOpen()) {
     69                 Cursor cursor = db.query("person", projection, selection, selectionArgs, null, null, sortOrder);
     70                 return cursor;
     71                 // db.close(); 返回cursor结果集时, 不可以关闭数据库
     72             }
     73             break;
     74         case PERSON_QUERY_ITEM_CODE:        // 查询的是单条数据, uri末尾出有一个id
     75             if(db.isOpen()) {
     76                 
     77                 long id = ContentUris.parseId(uri);
     78                 
     79                 Cursor cursor = db.query("person", projection, "_id = ?", new String[]{id + ""}, null, null, sortOrder);
     80 
     81                 return cursor;
     82             }
     83             break;
     84         default:
     85             throw new IllegalArgumentException("uri不匹配: " + uri);
     86         }
     87         return null;
     88     }
     89 
     90     @Override
     91     public Uri insert(Uri uri, ContentValues values) {
     92         
     93         switch (uriMatcher.match(uri)) {
     94         case PRESON_INSERT_CODE:    // 添加人到person表中
     95             SQLiteDatabase db = mOpenHelper.getWritableDatabase();
     96             
     97             if(db.isOpen()) {
     98                 
     99                 long id = db.insert("person", null, values);
    100                 
    101                 db.close();
    102                 
    103                 return ContentUris.withAppendedId(uri, id);
    104             }
    105             break;
    106         default:
    107             throw new IllegalArgumentException("uri不匹配: " + uri);
    108         }
    109         return null;
    110     }
    111 
    112     @Override
    113     public int delete(Uri uri, String selection, String[] selectionArgs) {
    114         switch (uriMatcher.match(uri)) {
    115         case PERSON_DELETE_CODE:    // 在person表中删除数据的操作
    116             SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    117             if(db.isOpen()) {
    118                 int count = db.delete("person", selection, selectionArgs);
    119                 db.close();
    120                 return count;
    121             }
    122             break;
    123         default:
    124             throw new IllegalArgumentException("uri不匹配: " + uri);
    125         }
    126         return 0;
    127     }
    128 
    129     @Override
    130     public int update(Uri uri, ContentValues values, String selection,
    131             String[] selectionArgs) {
    132         switch (uriMatcher.match(uri)) {
    133         case PERSON_UPDATE_CODE: // 更新person表的操作
    134             SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    135             if(db.isOpen()) {
    136                 int count = db.update("person", values, selection, selectionArgs);
    137                 db.close();
    138                 return count;
    139             }
    140             break;
    141         default:
    142             throw new IllegalArgumentException("uri不匹配: " + uri);
    143         }
    144         return 0;
    145     }
    146 
    147 }
    内容提供者
     1 import android.content.ContentResolver;
     2 import android.content.ContentUris;
     3 import android.content.ContentValues;
     4 import android.database.Cursor;
     5 import android.net.Uri;
     6 import android.test.AndroidTestCase;
     7 import android.util.Log;
     8 
     9 public class TextCase extends AndroidTestCase {
    10 
    11     private static final String TAG = "TextCase";
    12 
    13     public void testInsert() {
    14         Uri uri = Uri.parse("content://com.itheima28.sqlitedemo.providers.PersonContentProvider/person/insert");
    15         
    16         // 内容提供者访问对象
    17         ContentResolver resolver = getContext().getContentResolver();
    18         
    19         ContentValues values = new ContentValues();
    20         values.put("name", "fengjie");
    21         values.put("age", 90);
    22         
    23         uri = resolver.insert(uri, values);
    24         Log.i(TAG, "uri: " + uri);
    25         long id = ContentUris.parseId(uri);
    26         Log.i(TAG, "添加到: " + id);
    27     }
    28     
    29     public void testDelete() {
    30         Uri uri = Uri.parse("content://com.itheima28.sqlitedemo.providers.PersonContentProvider/person/delete");
    31         
    32         // 内容提供者访问对象
    33         ContentResolver resolver = getContext().getContentResolver();
    34         
    35         String where = "_id = ?";
    36         String[] selectionArgs = {"21"};
    37         int count = resolver.delete(uri, where, selectionArgs);
    38         Log.i(TAG, "删除行: " + count);
    39     }
    40     
    41     public void testUpdate() {
    42         Uri uri = Uri.parse("content://com.itheima28.sqlitedemo.providers.PersonContentProvider/person/update");
    43         
    44         // 内容提供者访问对象
    45         ContentResolver resolver = getContext().getContentResolver();
    46         
    47         ContentValues values = new ContentValues();
    48         values.put("name", "lisi");
    49         
    50         int count = resolver.update(uri, values, "_id = ?", new String[]{"20"});
    51         Log.i(TAG, "更新行: " + count);
    52     }
    53     
    54     public void testQueryAll() {
    55         Uri uri = Uri.parse("content://com.itheima28.sqlitedemo.providers.PersonContentProvider/person/queryAll");
    56         
    57         // 内容提供者访问对象
    58         ContentResolver resolver = getContext().getContentResolver();
    59         
    60         Cursor cursor = resolver.query(uri, new String[]{"_id", "name", "age"}, null, null, "_id desc");
    61         
    62         if(cursor != null && cursor.getCount() > 0) {
    63             
    64             int id;
    65             String name;
    66             int age;
    67             while(cursor.moveToNext()) {
    68                 id = cursor.getInt(0);
    69                 name = cursor.getString(1);
    70                 age = cursor.getInt(2);
    71                 Log.i(TAG, "id: " + id + ", name: " + name + ", age: " + age);
    72             }
    73             cursor.close();
    74         }
    75     }
    76     
    77     public void testQuerySingleItem() {
    78         Uri uri = Uri.parse("content://com.itheima28.sqlitedemo.providers.PersonContentProvider/person/query/#");
    79         
    80         // 在uri的末尾添加一个id content://com.itheima28.sqlitedemo.providers.PersonContentProvider/person/query/20
    81         uri = ContentUris.withAppendedId(uri, 20);
    82         
    83         // 内容提供者访问对象
    84         ContentResolver resolver = getContext().getContentResolver();
    85         
    86         Cursor cursor = resolver.query(uri, new String[]{"_id", "name", "age"}, null, null, null);
    87         
    88         if(cursor != null && cursor.moveToFirst()) {
    89             int id = cursor.getInt(0);
    90             String name = cursor.getString(1);
    91             int age = cursor.getInt(2);
    92             cursor.close();
    93             Log.i(TAG, "id: " + id + ", name: " + name + ", age: " + age);
    94         }
    95     }
    96 }
    访问内容提供者

    内容提供者是Android四大组件之一,是系统数据向外提供的方式,例如短信,联系人信息等,参考:http://www.2cto.com/kf/201312/261478.html

    和内容提供者相关的是内容观察者,指的是监听一个uri路径上的数据库变化并作出相应操作的类,参考:http://blog.csdn.net/qinjuning/article/details/7047607,注意这篇文章中的其他链接也有些不错的好文。

    参考:http://www.it165.net/pro/html/201402/9053.html.

    ⑤网络数据:Android是一个操作系统,里面各种应用进行网络操作,所以需要多种网络访问方式,而不像web中只有http即可,这就需要所谓的网络编程。参考:http://www.jb51.net/article/44859.htm

    但是移动端网络性能对用户体验影响大,所以通常需要多线程处理网络访问或者其他比较复杂的问题,这就涉及到多线程编程,而在多个线程之间进行消息的传递也是一个必须处理的问题,参考:http://blog.csdn.net/beiminglei/article/details/8474026

      1 import java.io.InputStream;
      2 import java.net.HttpURLConnection;
      3 import java.net.MalformedURLException;
      4 import java.net.URL;
      5 
      6 import javax.net.ssl.HttpsURLConnection;
      7 
      8 import android.os.Bundle;
      9 import android.os.Handler;
     10 import android.os.Message;
     11 import android.app.Activity;
     12 import android.graphics.Bitmap;
     13 import android.graphics.BitmapFactory;
     14 import android.util.Log;
     15 import android.view.Menu;
     16 import android.view.View;
     17 import android.view.View.OnClickListener;
     18 import android.widget.EditText;
     19 import android.widget.ImageView;
     20 import android.widget.Toast;
     21 
     22 public class MainActivity extends Activity implements OnClickListener {
     23 
     24     private static final String TAG = "MainActivity";
     25     protected static final int ERROR = 1;
     26     private EditText etUrl;
     27     private ImageView ivIcon;
     28     private final int SUCCESS = 0;
     29     
     30     private Handler handler = new Handler() {
     31 
     32         /**
     33          * 接收消息
     34          */
     35         @Override
     36         public void handleMessage(Message msg) {
     37             super.handleMessage(msg);
     38             
     39             Log.i(TAG, "what = " + msg.what);
     40             if(msg.what == SUCCESS) {    // 当前是访问网络, 去显示图片
     41                 ivIcon.setImageBitmap((Bitmap) msg.obj);        // 设置imageView显示的图片
     42             } else if(msg.what == ERROR) {
     43                 Toast.makeText(MainActivity.this, "抓去失败", 0).show();
     44             }
     45         }
     46     };
     47 
     48     @Override
     49     protected void onCreate(Bundle savedInstanceState) {
     50         super.onCreate(savedInstanceState);
     51         setContentView(R.layout.activity_main);
     52         
     53         ivIcon = (ImageView) findViewById(R.id.iv_icon);
     54         etUrl = (EditText) findViewById(R.id.et_url);
     55         
     56         findViewById(R.id.btn_submit).setOnClickListener(this);
     57     }
     58 
     59     @Override
     60     public void onClick(View v) {
     61         final String url = etUrl.getText().toString();
     62         
     63         new Thread(new Runnable() {
     64 
     65             @Override
     66             public void run() {
     67                 Bitmap bitmap = getImageFromNet(url);
     68 
     69 //                ivIcon.setImageBitmap(bitmap);        // 设置imageView显示的图片
     70                 if(bitmap != null) {
     71                     Message msg = new Message();
     72                     msg.what = SUCCESS;
     73                     msg.obj = bitmap;
     74                     handler.sendMessage(msg);
     75                 } else {
     76                     Message msg = new Message();
     77                     msg.what = ERROR;
     78                     handler.sendMessage(msg);
     79                 }
     80             }}).start();
     81         
     82     }
     83 
     84     /**
     85      * 根据url连接取网络抓去图片返回
     86      * @param url
     87      * @return url对应的图片
     88      */
     89     private Bitmap getImageFromNet(String url) {
     90         HttpURLConnection conn = null;
     91         try {
     92             URL mURL = new URL(url);    // 创建一个url对象
     93             
     94             // 得到http的连接对象
     95             conn = (HttpURLConnection) mURL.openConnection();
     96             
     97             conn.setRequestMethod("GET");        // 设置请求方法为Get
     98             conn.setConnectTimeout(10000);        // 设置连接服务器的超时时间, 如果超过10秒钟, 没有连接成功, 会抛异常
     99             conn.setReadTimeout(5000);        // 设置读取数据时超时时间, 如果超过5秒, 抛异常
    100             
    101             conn.connect();        // 开始链接
    102             
    103             int responseCode = conn.getResponseCode(); // 得到服务器的响应码
    104             if(responseCode == 200) {
    105                 // 访问成功
    106                 InputStream is = conn.getInputStream();    // 获得服务器返回的流数据
    107                 
    108                 Bitmap bitmap = BitmapFactory.decodeStream(is); // 根据 流数据 创建一个bitmap位图对象
    109                 
    110                 return bitmap;
    111             } else {
    112                 Log.i(TAG, "访问失败: responseCode = " + responseCode);
    113             }
    114         } catch (Exception e) {
    115             e.printStackTrace();
    116         } finally {
    117             if(conn != null) {
    118                 conn.disconnect();        // 断开连接
    119             }
    120         }
    121         return null;
    122     }
    123 }
    网络图片查看

     Android下进行http连接可以使用HttpClient类,但是在Android 6.0(即sdk23)之后移除了相关包,所以不推荐使用,但是好像有的框架依然在使用,可以作为了解,对于Android下使用参考http://liangruijun.blog.51cto.com/3061169/803097/:关于其他的使用参考:http://www.ibm.com/developerworks/cn/opensource/os-httpclient/。注意其中引用的文章列表。

    !!!网络访问在模拟器下测试时,访问本机的路径不是127.0.0.1,而是10.0.2.2,或者是局域网中的路径。参考:http://www.cnblogs.com/csulennon/p/3709032.html.

    二、android权限

    ①Android文件权限是linux风格的

    ②安卓的权限非常细,而且应用需要申请,而用户可以对其进行设定(在模拟器中直接赋予),

    具体权限参考:http://www.cnblogs.com/classic/archive/2011/06/20/2085055.html

    三、多线程下载:

    上图的要点是:

      ①RandomAccessFile类,这是一个非常适合用于多线程下载的类,因为它带有一个文件指针,可以方便的针对文件的指定位置进行操作。所以多个线程可以分别在不同的位置操作同一个文件而不出线程问题。参考:http://www.2cto.com/kf/201208/149816.html。注意:rwd是直接将读取的文件写入到硬盘上,而rw是先写到内存的缓冲区中,当达到一定量之后在写入硬盘。两者各有优点:一个防止特殊情况丢失下载数据(还没有保存就断网等情况),一个减少io操作。但是下载时使用rwd更好

      ②上面提到了多线程经常应用在网络等比较耗时的方面,对于网络下载更是如此。但是在Android下的下载使用的依然是j2se中的API,也就是HttpURLConnection类中的方法。具体实现的原理就是HTTP协议中的HTTP头 Range和Content-Range字段。参考:。

    所谓的端点下载就是在用一个临时文件同步存储下载数据,只有在下载完成时才删除,所以这个文件就用于判断是否下载完成和记录下载到了那里,便于之后继续下载。

      1 import java.io.BufferedReader;
      2 import java.io.File;
      3 import java.io.FileInputStream;
      4 import java.io.InputStream;
      5 import java.io.InputStreamReader;
      6 import java.io.RandomAccessFile;
      7 import java.net.HttpURLConnection;
      8 import java.net.URL;
      9 
     10 public class MutileThreadDownload {
     11     /**
     12      * 线程的数量
     13      */
     14     private static int threadCount = 3;
     15 
     16     /**
     17      * 每个下载区块的大小
     18      */
     19     private static long blocksize;
     20 
     21     /**
     22      * 正在运行的线程的数量
     23      */
     24     private static int runningThreadCount;
     25 
     26     /**
     27      * @param args
     28      * @throws Exception
     29      */
     30     public static void main(String[] args) throws Exception {
     31         // 服务器文件的路径
     32         String path = "http://192.168.1.100:8080/ff.exe";
     33         URL url = new URL(path);
     34         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
     35         conn.setRequestMethod("GET");
     36         conn.setConnectTimeout(5000);
     37         int code = conn.getResponseCode();
     38         if (code == 200) {
     39             long size = conn.getContentLength();// 得到服务端返回的文件的大小
     40             System.out.println("服务器文件的大小:" + size);
     41             blocksize = size / threadCount;
     42             // 1.首先在本地创建一个大小跟服务器一模一样的空白文件。
     43             File file = new File("temp.exe");
     44             RandomAccessFile raf = new RandomAccessFile(file, "rw");
     45             raf.setLength(size);
     46             // 2.开启若干个子线程分别去下载对应的资源。
     47             runningThreadCount = threadCount;
     48             for (int i = 1; i <= threadCount; i++) {
     49                 long startIndex = (i - 1) * blocksize;
     50                 long endIndex = i * blocksize - 1;
     51                 if (i == threadCount) {
     52                     // 最后一个线程
     53                     endIndex = size - 1;
     54                 }
     55                 System.out.println("开启线程:" + i + "下载的位置:" + startIndex + "~"
     56                         + endIndex);
     57                 new DownloadThread(path, i, startIndex, endIndex).start();
     58             }
     59         }
     60         conn.disconnect();
     61     }
     62 
     63     private static class DownloadThread extends Thread {
     64         private int threadId;
     65         private long startIndex;
     66         private long endIndex;
     67         private String path;
     68 
     69         public DownloadThread(String path, int threadId, long startIndex,
     70                 long endIndex) {
     71             this.path = path;
     72             this.threadId = threadId;
     73             this.startIndex = startIndex;
     74             this.endIndex = endIndex;
     75         }
     76 
     77         @Override
     78         public void run() {
     79             try {
     80                 // 当前线程下载的总大小
     81                 int total = 0;
     82                 File positionFile = new File(threadId + ".txt");
     83                 URL url = new URL(path);
     84                 HttpURLConnection conn = (HttpURLConnection) url
     85                         .openConnection();
     86                 conn.setRequestMethod("GET");
     87                 // 接着从上一次的位置继续下载数据
     88                 if (positionFile.exists() && positionFile.length() > 0) {// 判断是否有记录
     89                     FileInputStream fis = new FileInputStream(positionFile);
     90                     BufferedReader br = new BufferedReader(
     91                             new InputStreamReader(fis));
     92                     // 获取当前线程上次下载的总大小是多少
     93                     String lasttotalstr = br.readLine();
     94                     int lastTotal = Integer.valueOf(lasttotalstr);
     95                     System.out.println("上次线程" + threadId + "下载的总大小:"
     96                             + lastTotal);
     97                     startIndex += lastTotal;
     98                     total += lastTotal;// 加上上次下载的总大小。
     99                     fis.close();
    100                 }
    101 
    102                 conn.setRequestProperty("Range", "bytes=" + startIndex + "-"
    103                         + endIndex);
    104                 conn.setConnectTimeout(5000);
    105                 int code = conn.getResponseCode();
    106                 System.out.println("code=" + code);
    107                 InputStream is = conn.getInputStream();
    108                 File file = new File("temp.exe");
    109                 RandomAccessFile raf = new RandomAccessFile(file, "rw");
    110                 // 指定文件开始写的位置。
    111                 raf.seek(startIndex);
    112                 System.out.println("第" + threadId + "个线程:写文件的开始位置:"
    113                         + String.valueOf(startIndex));
    114                 int len = 0;
    115                 byte[] buffer = new byte[512];
    116                 while ((len = is.read(buffer)) != -1) {
    117                     RandomAccessFile rf = new RandomAccessFile(positionFile,
    118                             "rwd");
    119                     raf.write(buffer, 0, len);
    120                     total += len;
    121                     rf.write(String.valueOf(total).getBytes());
    122                     rf.close();
    123                 }
    124                 is.close();
    125                 raf.close();
    126 
    127             } catch (Exception e) {
    128                 e.printStackTrace();
    129             } finally {
    130                 // 只有所有的线程都下载完毕后 才可以删除记录文件。
    131                 synchronized (MutileThreadDownload.class) {
    132                     System.out.println("线程" + threadId + "下载完毕了");
    133                     runningThreadCount--;
    134                     if (runningThreadCount < 1) {
    135                         System.out.println("所有的线程都工作完毕了。删除临时记录的文件");
    136                         for (int i = 1; i <= threadCount; i++) {
    137                             File f = new File(i + ".txt");
    138                             System.out.println(f.delete());
    139                         }
    140                     }
    141                 }
    142 
    143             }
    144         }
    145     }
    146 }
    断点下载实例

    在Android上实现多线程下载的基本原理一样,只是有一些问题需要注意,

      ①下载文件的保存位置,Android项目需要保存在指定的位置:sd卡/Android项目文件路径

      ②多线程之间的信息传递

      ③对用户的提示

  • 相关阅读:
    刷题-力扣-剑指 Offer 22. 链表中倒数第k个节点
    刷题-力扣-617. 合并二叉树
    刷题-力扣-1109. 航班预订统计
    刷题-力扣-606. 根据二叉树创建字符串
    刷题-力扣-563. 二叉树的坡度
    扛把子组20191017-8 alpha week 2/2 Scrum立会报告+燃尽图 07
    20191017-7 alpha week 2/2 Scrum立会报告+燃尽图 06
    20191017-6alpha week 2/2 Scrum立会报告+燃尽图 05
    扛把子组20191017-5 alpha week 2/2 Scrum立会报告+燃尽图 04
    记第一次 团建
  • 原文地址:https://www.cnblogs.com/songfeilong2325/p/4829030.html
Copyright © 2011-2022 走看看