分類
Android

綁定型服務

綁定型服務是什麼?

綁定型服務是服務的一種,可以綁定應用程式元件並透過已定義的介面和元件互動。元件可以透過該介面呼叫服務的方法。
 

實作綁定型服務

透過繼承Service類別來建立綁定型服務,需要實作onBind方法,就是最簡單的綁定型服務。

public class BindService extends Service {
  public BindService() {
  }
  @Override
  public IBinder onBind(Intent intent) {
    // TODO: Return the communication channel to the service.
    throw new UnsupportedOperationException("Not yet implemented");
  }
}

重點為onBind方法,該方法會在元件綁定服務時執行,參數intent會從元件傳遞進來以攜帶需要從元件傳入的資料。
onBinder方法會回傳IBinder,當IBinder回傳到元件之後,元件便可透過IBinder和服務互動。
 

實作IBinder

IBinder是一個介面,其用途為讓元件取得服務的參考以進行和服務之間的互動。
因為Android已提供Binder類別,Binder類別已經實作IBinder介面中重要的方法。所以我們不必自己再實作IBinder介面,只需要建立Binder的子類別,並在該子類別中傳回服務的參考即可。

public class BindService extends Service {
  private IBinder mBinder = new ServiceBinder();
  public BindService() {
  }
  public class ServiceBinder extends Binder {
    public BindService getBindService() {
      return BindService.this;
    }
  }
  @Override
  public IBinder onBind(Intent intent) {
    return mBinder;
  }
  public int getRandom(){
   return new Random().nextInt(100);
  }
}

ServiceBinder類別的getBindService方法會回傳服務本身,元件就是透過這個方法取得服務的參考並與其互動。

實作ServiceConnection

當元件和服務進行綁定時是透過呼叫bindService方法來執行,如下
bindService(IntentToService, serviceConnect, Context.BIND_AUTO_CREATE);
該方法第2個參數為ServiceConnect物件,元件透過ServiceConnect物件取得目前和服務綁定的狀況(已連結或已中斷),我們需要建立ServiceConnect物件並實作2個方法,分別為

@Override
public void onServiceConnected(ComponentName name, IBinder service) {}

onServiceConnected方法會在元件綁定服務時被呼叫,該方法的IBinder參數即為從服務的onBind方法回傳,我們就可以透過該參數來取得服務。

@Override
public void onServiceDisconnected(ComponentName name) {}

onServiceDisconnected方法會在元件和服務中斷時被呼叫。
建立BindServiceActivity如下

public class BindServiceActivity extends AppCompatActivity {
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_bind_service);
  }
}

接著在ServiceConnection的onServiceConnected方法中實作透過傳入的binder取得服務的參考。

private BindService mBindService;
private ServiceConnection mServiceConnection = new ServiceConnection() {
  @Override
  public void onServiceConnected(ComponentName name, IBinder binder) {
    ServiceBinder serviceBinder = (ServiceBinder) binder;
    mBindService = serviceBinder.getBindService();
  }
  @Override
  public void onServiceDisconnected(ComponentName name) {
  }
};

另外在元件中還需要一個布林變數來記錄目前是否已綁定服務,這個變數主要是方便元件對服務進行綁定或解綁定。

private boolean mIsServiceBound;
private ServiceConnection mServiceConnection = new ServiceConnection() {
  @Override
  public void onServiceConnected(ComponentName name, IBinder binder) {
    ServiceBinder serviceBinder = (ServiceBinder) binder;
    mBindService = serviceBinder.getBindService();
    mIsServiceBound = true;
  }
  @Override
  public void onServiceDisconnected(ComponentName name) {
    mIsServiceBound = false;
  }
};

 

綁定服務或解綁定服務

以Activity來說綁定服務的實作位置通常會在onCreate或onStart,而解綁定的位置會對應綁定的位置。
若Activity移到後台時也要持續從服務收到更新,那就在onCreate綁定然後在onDestroy解綁定。
若Activity只有在前台顯示時需要收到服務的更新,在後台時不需要。就在onStart綁定然後在onStop解綁定。
以下在onCreate綁定服務,在onDestroy解綁定服務。

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_bind_service);
  Intent bindService = new Intent(this, BindService.class);
  if (!mIsServiceBound) {
    bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
  }
}
@Override
protected void onDestroy() {
  super.onDestroy();
  if (mIsServiceBound) {
    unbindService(mServiceConnection);
    mIsServiceBound = false;
  }
}

最後在Activity加入按鈕,按鈕按下時就會從服務取得亂數

@Override
public void onClick(View v) {
  int uiID = v.getId();
  switch (uiID) {
    case R.id.bind_service_get_service_random_number:
      if (mIsServiceBound) {
        Toast.makeText(this, "get Service random number:" + mBindService.getRandom(), Toast.LENGTH_SHORT).show();
      }
      break;
  }
}

 
以下為完整程式碼

public class BindServiceActivity extends AppCompatActivity implements OnClickListener {
  private BindService mBindService;
  private boolean mIsServiceBound;
  private Button mGetServiceRandomNumber;
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
      ServiceBinder serviceBinder = (ServiceBinder) binder;
      mBindService = serviceBinder.getBindService();
      mIsServiceBound = true;
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
      mIsServiceBound = false;
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_bind_service);
    mGetServiceRandomNumber = findViewById(R.id.bind_service_get_service_random_number);
    mGetServiceRandomNumber.setOnClickListener(this);
    Intent bindService = new Intent(this, BindService.class);
    if (!mIsServiceBound) {
      bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
    }
  }
  @Override
  protected void onDestroy() {
    super.onDestroy();
    if (mIsServiceBound) {
      unbindService(mServiceConnection);
      mIsServiceBound = false;
    }
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch (uiID) {
      case R.id.bind_service_get_service_random_number:
        if (mIsServiceBound) {
          Toast.makeText(this, "get Service random number:" + mBindService.getRandom(), Toast.LENGTH_SHORT).show();
        }
        break;
    }
  }
}
public class BindService extends Service {
  private IBinder mBinder = new ServiceBinder();
  public BindService() {
  }
  public class ServiceBinder extends Binder {
    public BindService getBindService() {
      return BindService.this;
    }
  }
  @Override
  public IBinder onBind(Intent intent) {
    return mBinder;
  }
  public int getRandom(){
   return new Random().nextInt(100);
  }
}

 

綁定型服務的生命週期

在BindService複寫onCreate, onBind, onStartCommand, onUnbind, onDestroy加入log來觀察生命週期。

public class BindService extends Service {
…
  public BindService() {
    Log.d(TAG, "BindService: ");
  }
  @Override
  public void onCreate() {
    Log.d(TAG, "onCreate: ");
    super.onCreate();
  }
  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
    Log.d(TAG, "onStartCommand: ");
    return super.onStartCommand(intent, flags, startId);
  }
  @Override
  public IBinder onBind(Intent intent) {
    Log.d(TAG, "onBind: ");
    return mBinder;
  }
  @Override
  public boolean onUnbind(Intent intent) {
    Log.d(TAG, "onUnbind: ");
    return super.onUnbind(intent);
  }
  @Override
  public void onDestroy() {
    Log.d(TAG, "onDestroy: ");
    super.onDestroy();
  }
…
}

在BindServiceActivity複寫onCreate, ServiceConnection的onServiceConnected, onServiceDisconnected加入log觀察生命週期

public class BindServiceActivity extends AppCompatActivity implements OnClickListener {
  private static final String TAG = BindServiceActivity.class.getSimpleName();
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
      ServiceBinder serviceBinder = (ServiceBinder) binder;
      mBindService = serviceBinder.getBindService();
      mIsServiceBound = true;
      Log.d(TAG, "onServiceConnected: ");
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
      mIsServiceBound = false;
      Log.d(TAG, "onServiceDisconnected: ");
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_bind_service);
    mGetServiceRandomNumber = findViewById(R.id.bind_service_get_service_random_number);
    mGetServiceRandomNumber.setOnClickListener(this);
    Intent bindService = new Intent(this, BindService.class);
    if (!mIsServiceBound) {
      Log.d(TAG, "Activity call bindService: ");
      bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
    }
  }
  @Override
  protected void onDestroy() {
    super.onDestroy();
    if (mIsServiceBound) {
      unbindService(mServiceConnection);
      Log.d(TAG, "Activity call unbindService: ");
      mIsServiceBound = false;
    }
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch (uiID) {
      case R.id.bind_service_get_service_random_number:
        if (mIsServiceBound) {
          Toast.makeText(this, "get Service random number:" + mBindService.getRandom(), Toast.LENGTH_SHORT).show();
        }
        break;
    }
  }
}

啟動Activity後的log為

D/BindServiceActivity: Activity call bindService:
D/BindService: BindService:
D/BindService: onCreate:
D/BindService: onBind:
D/BindServiceActivity: onServiceConnected:

需要注意的是onServiceConnected方法是在服務呼叫onBind方法之後呼叫
點擊back key之後的log為

D/BindServiceActivity: Activity call unbindService:
D/BindService: onUnbind:
D/BindService: onDestroy:

需要注意的是當元件透過呼叫unbindService去解綁定服務時, ServiceConnection的onServiceDisconnected方法不會被呼叫。
 

多個元件綁定和解綁定服務

綁定型服務可以和多個元件一起綁定。和多個元件綁定之後只有當所有的元件都解綁定該服務才會銷毀。
為了測試多個元件綁定服務,新增AnotherBindServiceActivity,如下

public class AnotherBindServiceActivity extends AppCompatActivity implements OnClickListener {
  private static final String TAG = AnotherBindServiceActivity.class.getSimpleName();
  private Button mBindServiceBtn;
  private Button mUnbindServiceBtn;
  private Button mGetRandomNumberBtn;
  private Button mLaunchBindServiceActivityBtn;
  private boolean mIsServiceBound;
  private BindService mBindService;
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
      ServiceBinder serviceBinder = (ServiceBinder) binder;
      mBindService = serviceBinder.getBindService();
      mIsServiceBound = true;
      Log.d(TAG, "onServiceConnected: ");
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
      mIsServiceBound = false;
      Log.d(TAG, "onServiceDisconnected: ");
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_another_bind_service);
    mLaunchBindServiceActivityBtn = findViewById(R.id.another_bind_service_start_bind_service_activity);
    mLaunchBindServiceActivityBtn.setOnClickListener(this);
    mBindServiceBtn = findViewById(R.id.another_bind_service_bind_service);
    mBindServiceBtn.setOnClickListener(this);
    mUnbindServiceBtn = findViewById(R.id.another_bind_service_unbind_service);
    mUnbindServiceBtn.setOnClickListener(this);
    mGetRandomNumberBtn = findViewById(R.id.another_bind_service_get_service_random_number);
    mGetRandomNumberBtn.setOnClickListener(this);
  }
  @Override
  protected void onResume() {
    super.onResume();
    Log.d(TAG, "AnotherBindServiceActivity is:"+this.toString());
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch(uiID){
      case R.id.another_bind_service_start_bind_service_activity:
        Intent launchBindServiceActivity = new Intent(this, BindServiceActivity.class);
        launchBindServiceActivity.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
        startActivity(launchBindServiceActivity);
        break;
      case R.id.another_bind_service_bind_service:
        Intent bindService = new Intent(this, BindService.class);
        if (!mIsServiceBound) {
          Log.d(TAG, "Activity call bindService: ");
          bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
        }
        break;
      case R.id.another_bind_service_unbind_service:
        if (mIsServiceBound) {
          unbindService(mServiceConnection);
          Log.d(TAG, "Activity call unbindService: ");
          mIsServiceBound = false;
        }
        break;
      case R.id.another_bind_service_get_service_random_number:
        if (mIsServiceBound) {
          Toast.makeText(this, "get Service random number:" + mBindService.getRandom()+" Service:"+mBindService.toString(), Toast.LENGTH_SHORT).show();
        }else{
          Toast.makeText(this, "Service not bind:", Toast.LENGTH_SHORT).show();
        }
        break;
    }
  }
}

重點在於AnotherBindServiceActivity也可綁定BindService,目前有BindServiceActivity和AnotherBindServiceActivity可綁定BindService。
另外修改BindServiceActivity新增按鍵用來綁定和解綁定以及啟動AnotherBindServiceActivity,如下

public class BindServiceActivity extends AppCompatActivity implements OnClickListener {
  private static final String TAG = BindServiceActivity.class.getSimpleName();
  private BindService mBindService;
  private boolean mIsServiceBound;
  private Button mGetServiceRandomNumberBtn;
  private Button mtBindServiceBtn;
  private Button mtUnBindServiceBtn;
  private Button mStartAnotherBindServiceActivityBtn;
  private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
      ServiceBinder serviceBinder = (ServiceBinder) binder;
      mBindService = serviceBinder.getBindService();
      mIsServiceBound = true;
      Log.d(TAG, "onServiceConnected: ");
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
      mIsServiceBound = false;
      Log.d(TAG, "onServiceConnected: ");
    }
  };
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_bind_service);
    mGetServiceRandomNumberBtn = findViewById(R.id.bind_service_get_service_random_number);
    mGetServiceRandomNumberBtn.setOnClickListener(this);
    mtBindServiceBtn = findViewById(R.id.bind_service_bind_service);
    mtBindServiceBtn.setOnClickListener(this);
    mtUnBindServiceBtn = findViewById(R.id.bind_service_unbind_service);
    mtUnBindServiceBtn.setOnClickListener(this);
    mStartAnotherBindServiceActivityBtn = findViewById(R.id.bind_service_start_another_bind_service_activity);
    mStartAnotherBindServiceActivityBtn.setOnClickListener(this);
  }
  @Override
  protected void onResume() {
    super.onResume();
    Log.d(TAG, "BindServiceActivity is:"+this.toString());
  }
  @Override
  protected void onDestroy() {
    super.onDestroy();
  }
  @Override
  public void onClick(View v) {
    int uiID = v.getId();
    switch (uiID) {
      case R.id.bind_service_get_service_random_number:
        if (mIsServiceBound) {
          Toast.makeText(this, "get Service random number:" + mBindService.getRandom()+" Service:"+mBindService.toString(), Toast.LENGTH_SHORT).show();
        }else{
          Toast.makeText(this, "Service not bind:", Toast.LENGTH_SHORT).show();
        }
        break;
      case R.id.bind_service_bind_service:
        Intent bindService = new Intent(this, BindService.class);
        if (!mIsServiceBound) {
          Log.d(TAG, "Activity call bindService: ");
          bindService(bindService, mServiceConnection, Context.BIND_AUTO_CREATE);
        }
        break;
      case R.id.bind_service_unbind_service:
        if (mIsServiceBound) {
          unbindService(mServiceConnection);
          Log.d(TAG, "Activity call unbindService: ");
          mIsServiceBound = false;
        }
        break;
      case R.id.bind_service_start_another_bind_service_activity:
        Intent launchAnotherBindServiceActivity = new Intent(this, AnotherBindServiceActivity.class);
        launchAnotherBindServiceActivity.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
        startActivity(launchAnotherBindServiceActivity);
        break;
    }
  }
}

注意BindServiceActivity和AnotherBindServiceActivity是透過FLAG_ACTIVITY_REORDER_TOFRONT來互相切換,並不是產生新的Activity實體。
現在先啟動BindServiceActivity並點擊bind service按鈕,Log如下

D/BindServiceActivity: Activity call bindService:
D/BindService: BindService:
D/BindService: onCreate:
D/BindService: onBind:
D/BindServiceActivity: onServiceConnected:

可以看出服務已被綁定,接著點擊按鈕啟動AnotherBindServiceActivity並在該Activity中點擊bind service按鈕,Log如下

D/AnotherBindServiceActivity: Activity call bindService:
D/AnotherBindServiceActivity: onServiceConnected:

AnotherBindServiceActivity綁定服務之後服務不會再呼叫建構式以及onCreate, onBind方法,因為該服務已啟動。
接著點擊AnotherBindServiceActivity的unbind service按鈕,Log如下

D/AnotherBindServiceActivity: Activity call unbindService:

可以看到該服務並未銷毀,只是和AnotherBindServiceActivity解綁定。
接著按下start BindServiceActivity按鈕來啟動BindServiceActivity,啟動之後點擊unbind service按鈕,Log如下。

D/BindServiceActivity: Activity call unbindService:
D/BindService: onUnbind:
D/BindService: onDestroy:

這時候因為所有和該服務綁定的元件都解綁定服務,服務銷毀。
 

綁定型服務的優缺點

綁定型服務的優點在於可以和多個元件互動,傳遞訊息。
缺點在於服務本身的執行也是在UI Thread中,沒有另外開啟新的執行緒來執行
 

分類
Android

授權處理

授權處理是什麼?

在某些情況下app對裝置的操作會影響隱私以及安全性等等,因此在app進行這些操作之前必須詢問使用者是否可以提供進行這些操作的權限。
這些權限必須宣告在AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
…
  <uses-permission android:name="android.permission.CAMERA"/>
…
</manifest>

要求權限的運作方式會根據app的targetSdkVersion和裝置的API level有不同的動作。
a.若app的targetSdkVersion大於等於23,且裝置的API level也是大於等於23。要求權限的動作會在app運作時,因此必須在執行這些權限前檢查是否已獲得授權,若已獲得授權就可執行相關動作,若未獲得授權可以要求使用者給予授權。
以上的機制也稱執行期權限檢查,也就是在運行app時才要求權限,安裝時不會要求授權。
b.若app的targetSdkVersion小於等於22,或裝置的API level也是小於等於22,要求權限的動作會在安裝app時請求授權,若使用者拒絕授權,app就無法安裝。若使用者給予授權,該授權就無法取消,除非重新安裝app。
以上的機制也稱安裝期權限檢查,不需進行處理,因為不給予授權即無法安裝app。
 

檢查是否已獲得授權

使用ContextCompat.checkSelfPermission方法

if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
}

第1個參數為context,第2個參數為要檢查的授權,若方法回傳值等於PackageManager.PERMISSION_GRANTED 代表已獲得授權,方法回傳值等於PackageManager.PERMISSION_DENIED 代表未獲得授權。

要求申請授權

使用ActivityCompat.requestPermissions方法

ActivityCompat.requestPermissions(this, new String[]{permission.CAMERA}, REQUEST_PERMISSION_CODE);

第1個參數為Activity,第2個參數為要申請的授權,第3個參數為要求授權碼,該要求授權碼在”處理申請授權結果”需要用到。
呼叫requestPermission之後會顯示對話框讓使用者選擇是否給予授權,使用者選擇完畢後會觸發onRequestPermissionsResult方法,因此在Activity也需要複寫該方法。
 

處理申請授權結果

在Activity複寫onRequestPermissionsResult 方法,並在該方法中處理使用者回復申請授權結果。

public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
  if (requestCode == REQUEST_PERMISSION_CODE) {
    if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        //使用者給予授權後進行動作
    } else {
        //使用者拒絕給予授權進行動作
    }
  }
}

if (requestCode == REQUEST_PERMISSION_CODE)
首先檢查要求授權碼是否相符,因為可能在多個位置呼叫請授權的動作。
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
代表使用者給予授權
 

處理使用者點擊Deny & don’t ask again

若使用者在app第1次申請權限時拒絕給予授權,則第2次申請授權時會另外出現Deny & don’t ask again選項,若使用者點擊該選項,即使之後再要求授權都不會出現申請授權的對話框。
也就是說呼叫requestPermissions方法不會顯示申請授權的對話框,但還是會觸發onRequestPermissionsResult方法且該方法的第3個參數grantResults會回傳-1,-1代表使用者拒絕給予授權。
此時已無法再申請授權,除了重新安裝app以外就是使用者手動打開權限設定的頁面才能申請授權。
因此可在onRequestPermissionsResult方法內呼叫shouldShowRequestPermissionRationale 方法。
若使用者點擊Deny & don’t ask again 則 shouldShowRequestPermissionRationale方法回傳false,此時可以建立對話框告知使用者是否開啟授權頁面,是的話導引使用者開啟授權頁面,否的話告知使用者無法進行後續動作。

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
    @NonNull int[] grantResults) {
  if (requestCode == REQUEST_PERMISSION_CODE) {
    if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
      startCameraActivity();
    } else {
      if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permission.CAMERA)) {
        AlertDialog.Builder builder = new Builder(this);
        builder.setTitle("Warning").setMessage("需要開啟相機權限,是否開啟權限頁面?").setPositiveButton("開啟權限頁面",
            new OnClickListener() {
              @Override
              public void onClick(DialogInterface dialogInterface, int i) {
                Intent intent = new Intent();
                intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                Uri uri = Uri.fromParts("package", getPackageName(), null);
                intent.setData(uri);
                startActivity(intent);
              }
            }).setNegativeButton("取消", null).show();
      } else {
        Toast.makeText(this, "需要授權以開啟相機", Toast.LENGTH_SHORT).show();
        Log.d(TAG, "onRequestPermissionsResult: user not click deny && don't ask");
      }
    }
  }
}

 
以上完成後可在該頁面加入按鈕讓使用者點擊後詢問是否開啟權限設定頁面

@Override
public void onClick(View view) {
  int uiID = view.getId();
  switch(uiID){
    case R.id.apply_permission:
      ActivityCompat.requestPermissions(this, new String[]{permission.CAMERA}, REQUEST_PERMISSION_CODE);
      break;
  }
}

 

重點總結

1.在AndroidManifest.xml宣告需要的授權
2. 檢查是否已獲得授權
已獲得授權->執行授權相關動作
未獲得授權->進入3.要求申請授權
3.要求申請授權
直接進入4.處理申請授權結果
4.處理申請授權結果
使用者給予授權->執行授權相關動作
使用者未給予授權->提示使用者給予授權,並提供再度進行申請授權的動作
完整程式碼如下

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
  private static final String TAG = MainActivity.class.getSimpleName();
  private static final int REQUEST_PERMISSION_CODE = 999;
  private Button mApplyPermissio;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mApplyPermissio = findViewById(R.id.apply_permission);
    mApplyPermissio.setOnClickListener(this);
    checkPermission();
  }
  private void checkPermission() {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
      startCameraActivity();
    } else {
      ActivityCompat.requestPermissions(this, new String[]{permission.CAMERA}, REQUEST_PERMISSION_CODE);
    }
  }
  @Override
  public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
      @NonNull int[] grantResults) {
    if (requestCode == REQUEST_PERMISSION_CODE) {
      if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        startCameraActivity();
      } else {
        if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permission.CAMERA)) {
          showOpenAppPermissionDialog();
        } else {
          Toast.makeText(this, "需要授權以開啟相機", Toast.LENGTH_SHORT).show();
        }
      }
    }
  }
  private void showOpenAppPermissionDialog() {
    Builder builder = new Builder(this);
    builder.setTitle("Warning").setMessage("需要開啟相機權限,是否開啟權限頁面?").setPositiveButton("開啟權限頁面",
        new OnClickListener() {
          @Override
          public void onClick(DialogInterface dialogInterface, int i) {
            Intent intent = new Intent();
            intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
            Uri uri = Uri.fromParts("package", getPackageName(), null);
            intent.setData(uri);
            startActivity(intent);
          }
        }).setNegativeButton("取消", new OnClickListener() {
      @Override
      public void onClick(DialogInterface dialogInterface, int i) {
        Toast.makeText(MainActivity.this, "請開啟權限設定並給予授權", Toast.LENGTH_SHORT).show();
      }
    }).show();
  }
  private void startCameraActivity() {
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    startActivityForResult(intent, 0);
  }
  @Override
  public void onClick(View view) {
    int uiID = view.getId();
    switch(uiID){
      case R.id.apply_permission:
        ActivityCompat.requestPermissions(this, new String[]{permission.CAMERA}, REQUEST_PERMISSION_CODE);
        break;
    }
  }
}

 
 
 
 
 

分類
Android

啟動型服務(IntentService)

IntentService是什麼?

IntentService是Service的一種,使用上較為方便簡單。其背景執行方式是透過 HandleThread,因此單個IntentService上的任務是循序執行的,可以保證執行緒安全。不同的IntentService在執行上也不會互相干擾。
IntentService適合用於客戶端不須和Service互動的情況下,基本上由客戶端呼叫startService並指定啟動的IntentService。


簡單的IntentService實作

public class SimpleIntentService extends IntentService {
  public SimpleIntentService() {
    super("SimpleIntentService");
  }
  @Override
  protected void onHandleIntent(Intent intent) {
  }
}

onHandleIntent方法會執行在新的執行緒上,也是放置耗時操作的位置。其參數intent就是從客戶端傳遞過來的intent。
另外需要在AndroidManifest.xml宣告該IntentService

<service
  android:exported="false"
  android:name=".simpleintentservice.SimpleIntentService">
</service>

android:exported=”false”代表是否可以由其他的App啟動。


啟動IntentService

客戶端透過呼叫startService方法來啟動IntentService

public class SimpleIntentServiceActivity extends AppCompatActivitye {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_simple_intent_service);
    Intent launchSimpleIntentService = new Intent(this, SimpleIntentService.class);
    startService(launchSimpleIntentService);
  }
}

客戶端傳遞資料到IntentService

從客戶端透過intent放置資料便可傳遞到IntentService

Intent launchSimpleIntentService = new Intent(this, SimpleIntentService.class);
launchSimpleIntentService.putExtra("data", "data from activity");
startService(launchSimpleIntentService);

透過IntentService的onHandleIntent的參數取出資料

@Override
protected void onHandleIntent(Intent intent) {
  String data = intent.getStringExtra("data");
  Log.d(TAG, "data:" + data);
}

output:

D: data:data from activity

IntentService的生命週期

複寫IntentService的生命週期方法觀察呼叫順序

public class SimpleIntentService extends IntentService {
  private static final String TAG = SimpleIntentService.class.getSimpleName();
  public SimpleIntentService() {
    super("SimpleIntentService");
    Log.d(TAG, "SimpleIntentService: ");
  }
  @Override
  public void onCreate() {
    super.onCreate();
    Log.d(TAG, "onCreate: ");
  }
  @Override
  public void onDestroy() {
    super.onDestroy();
    Log.d(TAG, "onDestroy: ");
  }
  @Override
  public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
    Log.d(TAG, "onStartCommand: ");
    return super.onStartCommand(intent, flags, startId);
  }
  @Override
  protected void onHandleIntent(Intent intent) {
    Log.d(TAG, "onHandleIntent: ");
    String data = intent.getStringExtra("data");
    Log.d(TAG, "data:" + data);
  }
}

Client call

Intent launchSimpleIntentService = new Intent(this, SimpleIntentService.class);
launchSimpleIntentService.putExtra("data", "data from activity");
startService(launchSimpleIntentService);
Log.d(TAG, "startService");

Output

D/SimpleIntentServiceActivity: startService
D/SimpleIntentService: SimpleIntentService:
D/SimpleIntentService: onCreate:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: onDestroy:

從Output觀察IntentService生命週期的順序為
建構式 -> onCreate -> onStartCommand -> onHandleIntent -> onDestroy


客戶端停止IntentService

透過呼叫stopService()方法來停止IntentService。

Intent stopSimpleIntentService = new Intent(this, SimpleIntentService.class);
stopService(stopSimpleIntentService);
Log.d(TAG, "stopService");

傳入的Intent必須指定要停止的IntentService。
呼叫StopService之後,IntentService便會呼叫onDestroy方法銷毀自己。
但一般的使用情境較多為IntentService完成onHandleIntent方法後自動呼叫onDestroy銷毀的情況
需要注意的是因為onHandleIntent方法內部是由新的執行緒來執行,因此即使是客戶端呼叫了stopService,但是onHandleIntent方法不會結束。
為了測試這點在onHandleIntent方法加入計時10秒的操作

@Override
protected void onHandleIntent(Intent intent) {
  Log.d(TAG, "onHandleIntent: ");
  String data = intent.getStringExtra("data");
  Log.d(TAG, "data:" + data);
  sleep(10);
}
private void sleep(int sleepTimeSeconds) {
  for (int i = 0; i < sleepTimeSeconds; ++i) {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    Log.d(TAG, "sleep: "+i);
  }
}

客戶端並在第3秒呼叫stopService
Output如下

D/SimpleIntentServiceActivity: startService
D/SimpleIntentService: SimpleIntentService:
D/SimpleIntentService: onCreate:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: sleep: 0
D/SimpleIntentService: sleep: 1
D/SimpleIntentService: sleep: 2
D/SimpleIntentService: sleep: 3
D/SimpleIntentServiceActivity: stopService
D/SimpleIntentService: onDestroy:
D/SimpleIntentService: sleep: 4
D/SimpleIntentService: sleep: 5
D/SimpleIntentService: sleep: 6
D/SimpleIntentService: sleep: 7
D/SimpleIntentService: sleep: 8
D/SimpleIntentService: sleep: 9

可以看到即使IntentService已經銷毀了,但計時仍然繼續。
因此若要呼叫stopService也一併停止onHandleIntent的內容,可以建立成員變數來控制是否要停止計時,如下

private boolean mIsDestroy;
@Override
public void onDestroy() {
  mIsDestroy = true;
  super.onDestroy();
  Log.d(TAG, "onDestroy: ");
}
private void sleep(int sleepTimeSeconds) {
  for (int i = 0; i < sleepTimeSeconds && !mIsDestroy; ++i) {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    Log.d(TAG, "sleep: "+i);
  }
}

IntentService的循序

IntentService在單一執行緒上執行任務,若客戶端呼叫多次startService方法,IntentService也會保持等待當前的任務完成後再執行下一個任務。
修改印出耗時資訊時也印出IntentService toString

  private void sleep(int sleepTimeSeconds) {
    for (int i = 0; i < sleepTimeSeconds && !mIsDestroy; ++i) {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      Log.d(TAG, "sleep: "+i+" name:"+toString());
    }
  }

連續啟動3次IntentService,Outout如下

D/SimpleIntentService: SimpleIntentService:
D/SimpleIntentService: onCreate:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: onDestroy:

可以看到每個任務都必須等到上一個任務完成後才執行。


不同IntentService的執行

觀察不同的IntentService執行的情況
新增另一個IntentService為AnotherSimpleIntentService如下

public class AnotherSimpleIntentService extends IntentService {
  private static final String TAG = AnotherSimpleIntentService.class.getSimpleName();
  private boolean mIsDestroy;
  public AnotherSimpleIntentService() {
    super("AnotherSimpleIntentService");
    Log.d(TAG, "AnotherSimpleIntentService: ");
  }
  @Override
  public void onCreate() {
    super.onCreate();
    Log.d(TAG, "onCreate: ");
  }
  @Override
  public void onDestroy() {
    mIsDestroy = true;
    super.onDestroy();
    Log.d(TAG, "onDestroy: ");
  }
  @Override
  public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
    Log.d(TAG, "onStartCommand: ");
    return super.onStartCommand(intent, flags, startId);
  }
  @Override
  protected void onHandleIntent(Intent intent) {
    Log.d(TAG, "onHandleIntent: ");
    String data = intent.getStringExtra("data");
    Log.d(TAG, "data:" + data);
    sleep(10);
  }
  private void sleep(int sleepTimeSeconds) {
    for (int i = 0; i < sleepTimeSeconds && !mIsDestroy; ++i) {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      Log.d(TAG, "sleep: "+i+" name:"+toString());
    }
  }
}

AndroidManifest.xml宣告AnotherSimpleIntentService

    <service
      android:exported="false"
      android:name=".simpleintentservice.AnotherSimpleIntentService">
    </service>

在客戶端依序啟動SimpleIntentService和AnotherSimpleIntentService

  public void onClick(View view) {
    int uiID = view.getId();
    switch (uiID) {
      case R.id.start_intent_service:
        startSimpleIntentService();
        startAnotherSimpleIntentService();
        break;
    }
  }
  private void startAnotherSimpleIntentService() {
    Intent launchAnotherSimpleIntentService = new Intent(this, AnotherSimpleIntentService.class);
    launchAnotherSimpleIntentService.putExtra("data", "data from activity");
    startService(launchAnotherSimpleIntentService);
    Log.d(TAG, "startService");
  }
  private void startSimpleIntentService() {
    Intent launchSimpleIntentService = new Intent(this, SimpleIntentService.class);
    launchSimpleIntentService.putExtra("data", "data from activity");
    startService(launchSimpleIntentService);
    Log.d(TAG, "startService");
  }

Output如下

D/SimpleIntentServiceActivity: startService
D/SimpleIntentServiceActivity: startService
D/SimpleIntentService: SimpleIntentService:
D/SimpleIntentService: onCreate:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/AnotherSimpleIntentService: AnotherSimpleIntentService:
D/AnotherSimpleIntentService: onCreate:
D/AnotherSimpleIntentService: onStartCommand:
D/AnotherSimpleIntentService: onHandleIntent:
D/AnotherSimpleIntentService: data:data from activity
D/SimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/SimpleIntentService: onDestroy:
D/AnotherSimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/AnotherSimpleIntentService: onDestroy:

可以看到不同的IntentService彼此不會互相影響,各自執行任務。


 

IntentService的優缺點

IntentService的優點在於容易使用,保持循序。
缺點在於不容易與客戶端進行互動(可以透過內部類別來解決),無法並行。

分類
Android

Error: Default interface methods are only supported starting with Android N (–min-api 24)

這是在 Android Studio 中套用了 androidx.core 之後出現的問題,build project 出現以下錯誤:

Error: Default interface methods are only supported starting with Android N (--min-api 24): android.view.MenuItem androidx.core.internal.view.SupportMenuItem.setContentDescription(java.lang.CharSequence)

解決方法有 2 種:
1. 在 /app/build.gradle 修改 minSdkVersion 為 24

android {
    ...
    defaultConfig {
        ...
        minSdkVersion 24
        ...
    }
    ...
}

 
2.在 /app/build.gradle 加入以下內容

android {
   ...
    compileOptions {
        sourceCompatibility = '1.8'
        targetCompatibility = '1.8'
    }
   ...
}

 

分類
Google Cloud Platform

Google Cloud Source Repositories(GCSR) 如何將遠端的 Repo clone 到本地端

Situation:

 想將 Google Cloud Source Repositories 已存在的遠端 repo clone 到本地端

 

Action : 

1.安裝 Google Cloud SDK(已安裝請忽略)

2.開啟 Google Cloud SDK Shell 並使用指令移動到想放置本地端 repo 的路徑

3.使用瀏覽器開啟 Cloud Source Repositories 的遠端 repo 頁面(以下為 hello-world 的示範頁面)

4.點擊右上方的”建立本機副本”並選擇 “Google Cloud SDK” 再複製”透過指令列複製存放區”的指令(這個指令就是把遠端 repo clone 到本地端的指令)

5.將第4步驟複製的指令貼到第2步驟開啟的 Google Cloud SDK Shell 中並執行(如下畫面)

 

Result : 

本地端應該可以看到從遠端 clone 下來的 repo)

分類
Android Architectural Pattern

Model in todo-mvp

概述

透過todo-mvp來說明MVP中的Model
todo-mvp 是 Android 官方用來說明 MVP Pattern的範例,參考 https://github.com/googlesamples/android-architecture
todo-mvp 裡的 Model 為TaskRepository,TaskRepository繼承TasksDataSource。
TaskDataSource實際上是一個interface,其中2個內部介面LoadTasksCallback和GetTasksCallback用來作callback使用,在內部介面的onTasksLoaded方法用來當取得task成功之後把task傳回呼叫點的用途,而onDataNotAvailable方法用來當取得task失敗後的後續處理。
其餘在TasksDataSource介面的方法都是存取資料的共用方法,只要是Model都要實作這些方法。
TasksDataSource.java

public interface TasksDataSource {
    interface LoadTasksCallback {
        void onTasksLoaded(List<Task> tasks);
        void onDataNotAvailable();
    }
    interface GetTaskCallback {
        void onTaskLoaded(Task task);
        void onDataNotAvailable();
    }
    void getTasks(@NonNull LoadTasksCallback callback);
    void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback);
    void saveTask(@NonNull Task task);
    void completeTask(@NonNull Task task);
    void completeTask(@NonNull String taskId);
    void activateTask(@NonNull Task task);
    void activateTask(@NonNull String taskId);
    void clearCompletedTasks();
    void refreshTasks();
    void deleteAllTasks();
    void deleteTask(@NonNull String taskId);
}

接著看Presenter如何關聯 Model以及使用Model。把焦點放在AddEditTaskPresenter。
1.AddEditTaskPresenter(Presenter)本身不會持有任何資料,資料放在Model中。
2.Presenter會通知Model去改變資料。
3.Presenter會持有Model和View的變數並在建構式初始化他們。
4.Presenter會在建構式初始化Model,接著在需要改變資料的位置去操縱Model改變資料,Model改變資料後Presenter再通知View重新載入資料。
AddEditTaskPresenter.java

public class AddEditTaskPresenter implements AddEditTaskContract.Presenter,
        TasksDataSource.GetTaskCallback {
    @NonNull
    private final TasksDataSource mTasksRepository;
    @NonNull
    private final AddEditTaskContract.View mAddTaskView;
    @Nullable
    private String mTaskId;
    private boolean mIsDataMissing;
    /**
     * Creates a presenter for the add/edit view.
     *
     * @param taskId ID of the task to edit or null for a new task
     * @param tasksRepository a repository of data for tasks
     * @param addTaskView the add/edit view
     * @param shouldLoadDataFromRepo whether data needs to be loaded or not (for config changes)
     */
    public AddEditTaskPresenter(@Nullable String taskId, @NonNull TasksDataSource tasksRepository,
            @NonNull AddEditTaskContract.View addTaskView, boolean shouldLoadDataFromRepo) {
        mTaskId = taskId;
        mTasksRepository = checkNotNull(tasksRepository);
        mAddTaskView = checkNotNull(addTaskView);
        mIsDataMissing = shouldLoadDataFromRepo;
        mAddTaskView.setPresenter(this);
    }
    @Override
    public void start() {
        if (!isNewTask() && mIsDataMissing) {
            populateTask();
        }
    }
    @Override
    public void saveTask(String title, String description) {
        if (isNewTask()) {
            createTask(title, description);
        } else {
            updateTask(title, description);
        }
    }
    @Override
    public void populateTask() {
        if (isNewTask()) {
            throw new RuntimeException("populateTask() was called but task is new.");
        }
        mTasksRepository.getTask(mTaskId, this);
    }
    @Override
    public void onTaskLoaded(Task task) {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.setTitle(task.getTitle());
            mAddTaskView.setDescription(task.getDescription());
        }
        mIsDataMissing = false;
    }
    @Override
    public void onDataNotAvailable() {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.showEmptyTaskError();
        }
    }
    @Override
    public boolean isDataMissing() {
        return mIsDataMissing;
    }
    private boolean isNewTask() {
        return mTaskId == null;
    }
    private void createTask(String title, String description) {
        Task newTask = new Task(title, description);
        if (newTask.isEmpty()) {
            mAddTaskView.showEmptyTaskError();
        } else {
            mTasksRepository.saveTask(newTask);
            mAddTaskView.showTasksList();
        }
    }
    private void updateTask(String title, String description) {
        if (isNewTask()) {
            throw new RuntimeException("updateTask() was called but task is new.");
        }
        mTasksRepository.saveTask(new Task(title, description, mTaskId));
        mAddTaskView.showTasksList(); // After an edit, go back to the list.
    }
}

注意雖然Model的變數型態為TasksDataSource(interface),但在Presenter建構式傳入的其實是TaskRepository(繼承自TasksDataSource)。
AddEditTaskActivity.java

...
        mPresenter = new AddEditTaskPresenter(
                taskId,
                Injection.provideTasksRepository(getApplicationContext()),
                addEditTaskView);
...

Injection.java

public class Injection {
    public static TasksRepository provideTasksRepository(@NonNull Context context) {
        checkNotNull(context);
        return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
                TasksLocalDataSource.getInstance(context));
    }
}

因此我們需要看的是TasksRepository的內容。
TasksRepository.java

public class TasksRepository implements TasksDataSource {
    private static TasksRepository INSTANCE = null;
    private final TasksDataSource mTasksRemoteDataSource;
    private final TasksDataSource mTasksLocalDataSource;
    /**
     * This variable has package local visibility so it can be accessed from tests.
     */
    Map<String, Task> mCachedTasks;
    /**
     * Marks the cache as invalid, to force an update the next time data is requested. This variable
     * has package local visibility so it can be accessed from tests.
     */
    boolean mCacheIsDirty = false;
    // Prevent direct instantiation.
    private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
                            @NonNull TasksDataSource tasksLocalDataSource) {
        mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
        mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
    }
    /**
     * Returns the single instance of this class, creating it if necessary.
     *
     * @param tasksRemoteDataSource the backend data source
     * @param tasksLocalDataSource  the device storage data source
     * @return the {@link TasksRepository} instance
     */
    public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,
                                              TasksDataSource tasksLocalDataSource) {
        if (INSTANCE == null) {
            INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
        }
        return INSTANCE;
    }
    /**
     * Used to force {@link #getInstance(TasksDataSource, TasksDataSource)} to create a new instance
     * next time it's called.
     */
    public static void destroyInstance() {
        INSTANCE = null;
    }
    /**
     * Gets tasks from cache, local data source (SQLite) or remote data source, whichever is
     * available first.
     * <p>
     * Note: {@link LoadTasksCallback#onDataNotAvailable()} is fired if all data sources fail to
     * get the data.
     */
    @Override
    public void getTasks(@NonNull final LoadTasksCallback callback) {
        checkNotNull(callback);
        // Respond immediately with cache if available and not dirty
        if (mCachedTasks != null && !mCacheIsDirty) {
            callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
            return;
        }
        if (mCacheIsDirty) {
            // If the cache is dirty we need to fetch new data from the network.
            getTasksFromRemoteDataSource(callback);
        } else {
            // Query the local storage if available. If not, query the network.
            mTasksLocalDataSource.getTasks(new LoadTasksCallback() {
                @Override
                public void onTasksLoaded(List<Task> tasks) {
                    refreshCache(tasks);
                    callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
                }
                @Override
                public void onDataNotAvailable() {
                    getTasksFromRemoteDataSource(callback);
                }
            });
        }
    }
    @Override
    public void saveTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.saveTask(task);
        mTasksLocalDataSource.saveTask(task);
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), task);
    }
    @Override
    public void completeTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.completeTask(task);
        mTasksLocalDataSource.completeTask(task);
        Task completedTask = new Task(task.getTitle(), task.getDescription(), task.getId(), true);
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), completedTask);
    }
    @Override
    public void completeTask(@NonNull String taskId) {
        checkNotNull(taskId);
        completeTask(getTaskWithId(taskId));
    }
    @Override
    public void activateTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.activateTask(task);
        mTasksLocalDataSource.activateTask(task);
        Task activeTask = new Task(task.getTitle(), task.getDescription(), task.getId());
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), activeTask);
    }
    @Override
    public void activateTask(@NonNull String taskId) {
        checkNotNull(taskId);
        activateTask(getTaskWithId(taskId));
    }
    @Override
    public void clearCompletedTasks() {
        mTasksRemoteDataSource.clearCompletedTasks();
        mTasksLocalDataSource.clearCompletedTasks();
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        Iterator<Map.Entry<String, Task>> it = mCachedTasks.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Task> entry = it.next();
            if (entry.getValue().isCompleted()) {
                it.remove();
            }
        }
    }
    /**
     * Gets tasks from local data source (sqlite) unless the table is new or empty. In that case it
     * uses the network data source. This is done to simplify the sample.
     * <p>
     * Note: {@link GetTaskCallback#onDataNotAvailable()} is fired if both data sources fail to
     * get the data.
     */
    @Override
    public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
        checkNotNull(taskId);
        checkNotNull(callback);
        Task cachedTask = getTaskWithId(taskId);
        // Respond immediately with cache if available
        if (cachedTask != null) {
            callback.onTaskLoaded(cachedTask);
            return;
        }
        // Load from server/persisted if needed.
        // Is the task in the local data source? If not, query the network.
        mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() {
            @Override
            public void onTaskLoaded(Task task) {
                // Do in memory cache update to keep the app UI up to date
                if (mCachedTasks == null) {
                    mCachedTasks = new LinkedHashMap<>();
                }
                mCachedTasks.put(task.getId(), task);
                callback.onTaskLoaded(task);
            }
            @Override
            public void onDataNotAvailable() {
                mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() {
                    @Override
                    public void onTaskLoaded(Task task) {
                        // Do in memory cache update to keep the app UI up to date
                        if (mCachedTasks == null) {
                            mCachedTasks = new LinkedHashMap<>();
                        }
                        mCachedTasks.put(task.getId(), task);
                        callback.onTaskLoaded(task);
                    }
                    @Override
                    public void onDataNotAvailable() {
                        callback.onDataNotAvailable();
                    }
                });
            }
        });
    }
    @Override
    public void refreshTasks() {
        mCacheIsDirty = true;
    }
    @Override
    public void deleteAllTasks() {
        mTasksRemoteDataSource.deleteAllTasks();
        mTasksLocalDataSource.deleteAllTasks();
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.clear();
    }
    @Override
    public void deleteTask(@NonNull String taskId) {
        mTasksRemoteDataSource.deleteTask(checkNotNull(taskId));
        mTasksLocalDataSource.deleteTask(checkNotNull(taskId));
        mCachedTasks.remove(taskId);
    }
    private void getTasksFromRemoteDataSource(@NonNull final LoadTasksCallback callback) {
        mTasksRemoteDataSource.getTasks(new LoadTasksCallback() {
            @Override
            public void onTasksLoaded(List<Task> tasks) {
                refreshCache(tasks);
                refreshLocalDataSource(tasks);
                callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values()));
            }
            @Override
            public void onDataNotAvailable() {
                callback.onDataNotAvailable();
            }
        });
    }
    private void refreshCache(List<Task> tasks) {
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.clear();
        for (Task task : tasks) {
            mCachedTasks.put(task.getId(), task);
        }
        mCacheIsDirty = false;
    }
    private void refreshLocalDataSource(List<Task> tasks) {
        mTasksLocalDataSource.deleteAllTasks();
        for (Task task : tasks) {
            mTasksLocalDataSource.saveTask(task);
        }
    }
    @Nullable
    private Task getTaskWithId(@NonNull String id) {
        checkNotNull(id);
        if (mCachedTasks == null || mCachedTasks.isEmpty()) {
            return null;
        } else {
            return mCachedTasks.get(id);
        }
    }
}

TasksRepository實現3層緩存,首先第1層緩存為記憶體也就是Map<String, Task> mCachedTasks;
第2層緩存為本地端資料來源,也就是private final TasksDataSource mTasksLocalDataSource;
因為該變數的型態也是TasksDataSource為interface,因此不會被資料來源的實現綁住,也就是說若想更換不同的資料庫,也只要新增TasksDataSource的子類別繼承TasksDataSource即可(OCP)。
第3層緩存為遠端資料來源,為 private final TasksDataSource mTasksRemoteDataSource;
變數型態也是TasksDataSource,也可以簡單替換遠端來源的實現(OCP),如volley, okhttp, retrofit等等。
若以儲存資料來說,在順序性來說沒有分別,這3層都會儲存資料,如下面的TasksRepository.saveTask方法的實作內容

    @Override
    public void saveTask(@NonNull Task task) {
        checkNotNull(task);
        mTasksRemoteDataSource.saveTask(task);
        mTasksLocalDataSource.saveTask(task);
        // Do in memory cache update to keep the app UI up to date
        if (mCachedTasks == null) {
            mCachedTasks = new LinkedHashMap<>();
        }
        mCachedTasks.put(task.getId(), task);
    }

若是讀取資料,則會先從第1層緩存記憶體(mCachedTasks)去讀取資料,若資料存在就直接回傳,若資料不存在,則從第2層緩存本地端資料庫(mTasksLocalDataSource)去讀取資料,若有資料則把該資料加到記憶體(mCachedTasks)後再回傳資料。
若還是沒有資料則從第3層緩存遠端網路(mTasksRemoteDataSource)去讀取資料,若有資料則把該資料加到記憶體(mCachedTasks)後再回傳資料,若資料不存在則顯示該資料不存在訊息。
如下方的TasksRepository.getTask方法內容

    @Override
    public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
        checkNotNull(taskId);
        checkNotNull(callback);
        Task cachedTask = getTaskWithId(taskId);
        // Respond immediately with cache if available
        if (cachedTask != null) {
            callback.onTaskLoaded(cachedTask);
            return;
        }
        // Load from server/persisted if needed.
        // Is the task in the local data source? If not, query the network.
        mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() {
            @Override
            public void onTaskLoaded(Task task) {
                // Do in memory cache update to keep the app UI up to date
                if (mCachedTasks == null) {
                    mCachedTasks = new LinkedHashMap<>();
                }
                mCachedTasks.put(task.getId(), task);
                callback.onTaskLoaded(task);
            }
            @Override
            public void onDataNotAvailable() {
                mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() {
                    @Override
                    public void onTaskLoaded(Task task) {
                        // Do in memory cache update to keep the app UI up to date
                        if (mCachedTasks == null) {
                            mCachedTasks = new LinkedHashMap<>();
                        }
                        mCachedTasks.put(task.getId(), task);
                        callback.onTaskLoaded(task);
                    }
                    @Override
                    public void onDataNotAvailable() {
                        callback.onDataNotAvailable();
                    }
                });
            }
        });
    }

接著來看看在TasksRepository建構式,存取權限為私有,代表只能透過該纇別內部呼叫。

    // Prevent direct instantiation.
    private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
                            @NonNull TasksDataSource tasksLocalDataSource) {
        mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
        mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
    }

呼叫該建構式的位置為getInstance()方法,會透過其方法的參數設定tasksRemoteDataSource和tasksLocalDataSource。

    public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource,
                                              TasksDataSource tasksLocalDataSource) {
        if (INSTANCE == null) {
            INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource);
        }
        return INSTANCE;
    }

而getInstance方法的呼叫者為Injection類別的ProvideTasksRepository方法。

public class Injection {
    public static TasksRepository provideTasksRepository(@NonNull Context context) {
        checkNotNull(context);
        ToDoDatabase database = ToDoDatabase.getInstance(context);
        return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(),
                TasksLocalDataSource.getInstance(new AppExecutors(),
                        database.taskDao()));
    }
}

可以看到provideTasksRepository方法內即為FakeTasksRemoteDataSource.getIntance()和TasksLocalDataSource.getInstance()分別代表遠端資料來源和本地端資料來源。
接著看看FakeTasksRemoteDataSource類別。其內容相當簡單,儲存資料的方式是透過一個Map<String, Task> TASKS_SERVICE_DATA 來儲存資料。
FakeTasksRemoteDataSource.java

public class FakeTasksRemoteDataSource implements TasksDataSource {
    private static FakeTasksRemoteDataSource INSTANCE;
    private static final Map<String, Task> TASKS_SERVICE_DATA = new LinkedHashMap<>();
    // Prevent direct instantiation.
    private FakeTasksRemoteDataSource() {}
    public static FakeTasksRemoteDataSource getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new FakeTasksRemoteDataSource();
        }
        return INSTANCE;
    }
    @Override
    public void getTasks(@NonNull LoadTasksCallback callback) {
        callback.onTasksLoaded(Lists.newArrayList(TASKS_SERVICE_DATA.values()));
    }
    @Override
    public void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback) {
        Task task = TASKS_SERVICE_DATA.get(taskId);
        callback.onTaskLoaded(task);
    }
    @Override
    public void saveTask(@NonNull Task task) {
        TASKS_SERVICE_DATA.put(task.getId(), task);
    }
    @Override
    public void completeTask(@NonNull Task task) {
        Task completedTask = new Task(task.getTitle(), task.getDescription(), task.getId(), true);
        TASKS_SERVICE_DATA.put(task.getId(), completedTask);
    }
    @Override
    public void completeTask(@NonNull String taskId) {
        // Not required for the remote data source.
    }
    @Override
    public void activateTask(@NonNull Task task) {
        Task activeTask = new Task(task.getTitle(), task.getDescription(), task.getId());
        TASKS_SERVICE_DATA.put(task.getId(), activeTask);
    }
    @Override
    public void activateTask(@NonNull String taskId) {
        // Not required for the remote data source.
    }
    @Override
    public void clearCompletedTasks() {
        Iterator<Map.Entry<String, Task>> it = TASKS_SERVICE_DATA.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Task> entry = it.next();
            if (entry.getValue().isCompleted()) {
                it.remove();
            }
        }
    }
    public void refreshTasks() {
        // Not required because the {@link TasksRepository} handles the logic of refreshing the
        // tasks from all the available data sources.
    }
    @Override
    public void deleteTask(@NonNull String taskId) {
        TASKS_SERVICE_DATA.remove(taskId);
    }
    @Override
    public void deleteAllTasks() {
        TASKS_SERVICE_DATA.clear();
    }
    @VisibleForTesting
    public void addTasks(Task... tasks) {
        for (Task task : tasks) {
            TASKS_SERVICE_DATA.put(task.getId(), task);
        }
    }
}

 
最後看看 TasksLocalDataSource 類別。
該類別有TaskDao以及AppExecutors 變數,其中TaskDao提供存取Task的介面,為使用Room的寫法。關於Room可以參考這篇
而AppExecutors主要負責Executor的執行。
TasksLocalDataSource.java

public class TasksLocalDataSource implements TasksDataSource {
    private static volatile TasksLocalDataSource INSTANCE;
    private TasksDao mTasksDao;
    private AppExecutors mAppExecutors;
    // Prevent direct instantiation.
    private TasksLocalDataSource(@NonNull AppExecutors appExecutors,
            @NonNull TasksDao tasksDao) {
        mAppExecutors = appExecutors;
        mTasksDao = tasksDao;
    }
    public static TasksLocalDataSource getInstance(@NonNull AppExecutors appExecutors,
            @NonNull TasksDao tasksDao) {
        if (INSTANCE == null) {
            synchronized (TasksLocalDataSource.class) {
                if (INSTANCE == null) {
                    INSTANCE = new TasksLocalDataSource(appExecutors, tasksDao);
                }
            }
        }
        return INSTANCE;
    }
    /**
     * Note: {@link LoadTasksCallback#onDataNotAvailable()} is fired if the database doesn't exist
     * or the table is empty.
     */
    @Override
    public void getTasks(@NonNull final LoadTasksCallback callback) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                final List<Task> tasks = mTasksDao.getTasks();
                mAppExecutors.mainThread().execute(new Runnable() {
                    @Override
                    public void run() {
                        if (tasks.isEmpty()) {
                            // This will be called if the table is new or just empty.
                            callback.onDataNotAvailable();
                        } else {
                            callback.onTasksLoaded(tasks);
                        }
                    }
                });
            }
        };
        mAppExecutors.diskIO().execute(runnable);
    }
    /**
     * Note: {@link GetTaskCallback#onDataNotAvailable()} is fired if the {@link Task} isn't
     * found.
     */
    @Override
    public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                final Task task = mTasksDao.getTaskById(taskId);
                mAppExecutors.mainThread().execute(new Runnable() {
                    @Override
                    public void run() {
                        if (task != null) {
                            callback.onTaskLoaded(task);
                        } else {
                            callback.onDataNotAvailable();
                        }
                    }
                });
            }
        };
        mAppExecutors.diskIO().execute(runnable);
    }
    @Override
    public void saveTask(@NonNull final Task task) {
        checkNotNull(task);
        Runnable saveRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.insertTask(task);
            }
        };
        mAppExecutors.diskIO().execute(saveRunnable);
    }
    @Override
    public void completeTask(@NonNull final Task task) {
        Runnable completeRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.updateCompleted(task.getId(), true);
            }
        };
        mAppExecutors.diskIO().execute(completeRunnable);
    }
    @Override
    public void completeTask(@NonNull String taskId) {
        // Not required for the local data source because the {@link TasksRepository} handles
        // converting from a {@code taskId} to a {@link task} using its cached data.
    }
    @Override
    public void activateTask(@NonNull final Task task) {
        Runnable activateRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.updateCompleted(task.getId(), false);
            }
        };
        mAppExecutors.diskIO().execute(activateRunnable);
    }
    @Override
    public void activateTask(@NonNull String taskId) {
        // Not required for the local data source because the {@link TasksRepository} handles
        // converting from a {@code taskId} to a {@link task} using its cached data.
    }
    @Override
    public void clearCompletedTasks() {
        Runnable clearTasksRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.deleteCompletedTasks();
            }
        };
        mAppExecutors.diskIO().execute(clearTasksRunnable);
    }
    @Override
    public void refreshTasks() {
        // Not required because the {@link TasksRepository} handles the logic of refreshing the
        // tasks from all the available data sources.
    }
    @Override
    public void deleteAllTasks() {
        Runnable deleteRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.deleteTasks();
            }
        };
        mAppExecutors.diskIO().execute(deleteRunnable);
    }
    @Override
    public void deleteTask(@NonNull final String taskId) {
        Runnable deleteRunnable = new Runnable() {
            @Override
            public void run() {
                mTasksDao.deleteTaskById(taskId);
            }
        };
        mAppExecutors.diskIO().execute(deleteRunnable);
    }
    @VisibleForTesting
    static void clearInstance() {
        INSTANCE = null;
    }
}

整體來說因為Model提供了三層緩存,複雜度反而比Presenter和View還高。
從裡面的設計也可以看到設計原則(OCP)等等。
以上便是 todo-mvp 的 Model 。


以下為MVP相關內容

MVP Pattern in Android

分類
Android Uncategorized

如何從 adb 啟動 App 並帶參數

概述

如何從adb啟動App並帶參數

做法

以todo-app為例,使用adb啟動App使用的指令為

adb shell am start -n [PACKAGE-NAME]/[ACTIVITY-NAME]

因此需要先找到PACKAGE-NAME 和 ACTIVITY-NAME

1.找PACKAGE-NAME

先安裝 todo App 到裝置上
1.1輸入以下指令便會列出 App 上所有已安裝的 PACKAGE-NAME

adb shell pm list packages -f

若連接多台裝置則使用 -s 指定裝置號碼如下

adb -s DeviceNumber shell pm listpackages -f

如何取得裝置號碼則使用 adb devices
輸入adb shell pm list packages -f 之後回傳的內容可能太長,因此可以在指令的最後加上 > D:\testlog\get_packages.txt 將顯示內容輸出到D:\testlog\get_packages.txt
因此輸入

adb -s DeviceNumber shell pm list packages -f > D:\testlog\get_packages.txt

在d:\testlog\get_packages.txt尋找todo關鍵字,只找到一項如下

package:/data/app/com.example.android.architecture.blueprints.todomvp.mock-1/base.apk=com.example.android.architecture.blueprints.todomvp.mock

 

2.找ACTIVITY-NAME

輸入 adb shell dumpsys activity 便可列出正在 App 上執行的所有 Activity
注意,App必須正在執行中才會顯示 activity name
2.1先在裝置上啟動 todo-app
2.2 輸入

adb -s DeviceNumber shell dumpsys activity -f > D:\testlog\get_activities.txt

2.3在D:\testlog\get_activities.txt 尋找 todo關鍵字,找到其中一個區塊如下

…
TaskRecord{cc64a79 #6942 A=com.example.android.architecture.blueprints.todomvp.mock U=0 StackId=1 sz=1}
      Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity bnds=[18,1035][268,1320] (has extras) }
        Hist #0: ActivityRecord{199dedc u0 com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity t6942}
          Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity bnds=[18,1035][268,1320] (has extras) }
          ProcessRecord{66be5e 5459:com.example.android.architecture.blueprints.todomvp.mock/u0a566}
    Running activities (most recent first):
      TaskRecord{cc64a79 #6942 A=com.example.android.architecture.blueprints.todomvp.mock U=0 StackId=1 sz=1}
        Run #0: ActivityRecord{199dedc u0 com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity t6942}
    mResumedActivity: ActivityRecord{199dedc u0 com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity t6942}
…

在該區塊中尋找關鍵字cmp,可以找到以下內容

cmp=com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity

cmp=後方的內容其實就是PACKAGE-NAME/ACTIVITY-NAME
因此可以輸入下方指令來啟動todo App了

adb -s DeviceNumber shell am start -n com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity

成功啟動便會回應

Starting: Intent { cmp=com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity }

並在裝置上啟動todo app。

3.啟動App附帶參數

啟動App附帶參數的方式為在指令的最後加入 –es key ‘value’ 或 –ez key ‘value’或 –ei key ‘value’ 或 –ef key ‘value’
其中 –es 參數為string, –ez 參數為boolean, –ei參數型態為int, –ef參數為float。
若要取得參數,則在該Activity的onCreate方法中使用 getIntent().getXXXExtra的方式,xxx則視傳入參數為甚麼型態而定,若現在想啟動todo app並帶參數為string型態,參數的key為test, 參數的value為 test parameter from adb
,則輸入指令如下

adb -s DeviceNumber shell am start -n com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity --es test 'test parameter from adb'

 
在TasksActivity的onCreate方法中加入

Log.d(TAG, "onCreate: "+getIntent().getStringExtra("test"));

觀察logcat輸出便可看到 onCreate: test parameter from adb,代表使用adb成功啟動App並附帶參數。
 

分類
Android

使用 Stetho 查看實體或虛擬裝置的資料庫內容

概述

Stetho 為 Facebook 出品的開源 Android 調試工具(官網連結),主要功能有網路抓包,查看資料庫,查看視圖階層等等。
本篇主要描述如何使用 Stetho 查看實體裝置的資料庫內容。

步驟

1.Dependencies

在 Module 的 build.gradle 加入以下內容

dependencies {
…
    implementation 'com.facebook.stetho:stetho:1.5.0'
}

 

2.在 App 的 Source Code 初始化 Stetho

在 App 第一個啟動 Activity 的 onCreate 方法或新增一個類別繼承 Application 的 onCreate方法加入Stetho.initializeWithDefaults(this);

public class SingletonApplication extends Application {
        public void onCreate() {
        super.onCreate();
        Stetho.initializeWithDefaults(this);
    }
}

 

3.啟動 Stetho 工具

開啟 chrome 瀏覽器並輸入 chrome://inspect 就會開啟 Stetho 工具,Stetho 工具的用途為提供管理所有可調試元件的介面。在紅框內即為連接的實體裝置,虛擬裝置也行。

4.啟動 App

啟動 App 後可以在 Stetho 工具的 Remote Target 看到啟動的 App 名稱
以 todo-app 為例。

點擊 inspect 便會跳出 DevTools 網頁,DevTools 網頁為該 App 專屬的調試工具,點擊網頁上方的 Resources -> 左方 Web SQL 就會顯示該 App 所使用的 db 檔(資料庫),再展開 db 檔(資料庫),內容為即為該資料庫內的資料表。
 
以 todo-app 為例,下方為啟動 App 後一開始的資料庫和資料表範例。
Tasks.db 為資料庫,其中有 tasks 資料表,但因為目前還未新增資料因此資料表沒有內容。

接著在 App 中新增資料,完成後點擊 DevTools 下方有個刷新的圖示或是再次點擊 tasks,就會顯示剛剛新增資料的內容(如紅框)

以上為使用 Stetho 觀察實體裝置的資料庫內容方法。

分類
Android

Error Type 3 in Android Studio

描述

在 Android Studio 3.3 啟動 App 時出現有關 Error Type 3 的錯誤,可以參考https://stackoverflow.com/questions/20915266/error-type-3-error-activity-class-does-not-exist

原因

目前看起來是啟用 Instance Run 會觸發該問題,但真正的原因未知。

解決

到 File -> Settings -> Build, Execution, Deployment -> Instant Run -> 關閉 Enable Instant Run to hot swap code/resource changes on deploy (default enabled)

分類
Android Architectural Pattern

MVP Pattern in Android

概述

(關於 MVC Pattern in Android 可以參考這篇,本篇是套用 MVP 於 Android 的描述和實作)
MVP 將架構分為 3 個部分,分別為 Model(模型層),View(視圖層),Presenter(展示層)
View(視圖層): 負責與使用者互動並顯示來自展示層的資料。
Presenter(展示層): 負責處理視圖邏輯和互動邏輯。
Model(模型層): 負責管理業務邏輯,提供網路和資料庫的監聽和修改介面。

設計理念

當 Model 傳輸資料到 Presenter 時,Presenter 會封裝視圖邏輯後再把資料傳遞給 View。和 MVC 最大的不同為 MVP 將視圖邏輯從 Model 移動到 Presenter。而 Model 只保留業務邏輯,從而分離視圖邏輯和業務邏輯,且 View 和 Model 完全分離不會有任何相依關係。
View 和 Presenter 只透過介面的方法互相溝通,不依賴具體方法。
Presenter 不依賴於 Android 的 UI 元件,也就是在 Presenter 不會出現 Toast, Button, Context 等等,Presenter 處理完邏輯之後再呼叫 View 顯示內容。

Contract 介面

在 Google 所提供 MVP 範例中可以看到 View 和 Presenter 的介面互相對應,為了描述其相對應的關係,每對 View 和 Presenter 的介面都會放置於其 Contract 介面。e.g.,

public interface AddEditTaskContract {
    interface View extends BaseView<Presenter> {
        void showEmptyTaskError();
        void showTasksList();
        void setTitle(String title);
        void setDescription(String description);
        boolean isActive();
    }
    interface Presenter extends BasePresenter {
        void saveTask(String title, String description);
        void populateTask();
    }
}

BaseView 和 BasePresetner 介面定義了 View 層和 Presenter 層的公共接口,在 BaseView 會提供 setPresenter 方法以及泛型來讓 View 指定對應的 Presenter。e.g.,

public interface BaseView<T> {
    void setPresenter(T presenter);
}

BasePresenter 則是提供公共的啟動方法 start。e.g.,

public interface BasePresenter {
    void start();
}

View

在 MVP 架構中,View 和 Presenter 互相對應,View 用來顯示 Presenter 的資料。先將使用者輸入事件轉發給 Presenter,當 Presenter 處理完邏輯後,再呼叫 View 顯示內容。

  1. View 和 Presenter 的交互都會使用其介面的方法來呼叫,而不會呼叫具體的方法。
  2. View和Presenter的互相關連是透過 setPresenter 方法,該方法會在 Presenter 的建構式呼叫。
  3. View 通常提供更新畫面的方法讓 Presenter 在執行完邏輯後呼叫。

在 Google 範例中 View 比較特別,它不是 Activity,而是一個自定義類別,該類別會使用自定義的 xml,xml 中會包含其它的 UI 元件。e.g.,
AddEditTaskView.java(View)

public class AddEditTaskView extends ScrollView implements AddEditTaskContract.View {
    private TextView mTitle;
    private TextView mDescription;
    private AddEditTaskContract.Presenter mPresenter;
    private boolean mActive;
    public AddEditTaskView(Context context) {
        super(context);
        init();
    }
    public AddEditTaskView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init() {
        inflate(getContext(), R.layout.addtask_view_content, this);
        mTitle = (TextView) findViewById(R.id.add_task_title);
        mDescription = (TextView) findViewById(R.id.add_task_description);
        mActive = true;
    }
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        mActive = true;
    }
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mActive = false;
    }
    @Override
    public void showEmptyTaskError() {
        Snackbar.make(mTitle,
                getResources().getString(R.string.empty_task_message), Snackbar.LENGTH_LONG).show();
    }
    @Override
    public void showTasksList() {
        Activity activity = getActivity(this);
        activity.setResult(Activity.RESULT_OK);
        activity.finish();
    }
    @Override
    public void setTitle(String title) {
        mTitle.setText(title);
    }
    @Override
    public void setDescription(String description) {
        mDescription.setText(description);
    }
    @Override
    public boolean isActive() {
        return mActive;
    }
    @Override
    public void setPresenter(AddEditTaskContract.Presenter presenter) {
        mPresenter = checkNotNull(presenter);
    }
    // TODO: This should be in the view contract
    public String getTitle() {
        return mTitle.getText().toString();
    }
    // TODO: This should be in the view contract
    public String getDescription() {
        return mDescription.getText().toString();
    }
}

在 init 方法中 R.layout.addtask_view_content = addtask_view_content.xml e.g.

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin">
        <EditText
            android:id="@+id/add_task_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/title_hint"
            android:singleLine="true"
            android:textAppearance="@style/TextAppearance.AppCompat.Title" />
        <EditText
            android:id="@+id/add_task_description"
            android:layout_width="match_parent"
            android:layout_height="350dp"
            android:gravity="top"
            android:hint="@string/description_hint" />
    </LinearLayout>
</merge>

簡單的說 View 是 Activity 內的一個 UI 元件,會以變數的方式存在於 Activity中。該變數的初始化也會使用 findViewById 方式初始化,初始化的位置在onCreate 中,而 View 和 Presenter 的相互關聯在 Presenter 的建構式。e.g.,
AddEditTaskActivity.java(這是 Activity 不是 View)

public class AddEditTaskActivity extends AppCompatActivity {
    public static final int REQUEST_ADD_TASK = 1;
    public static final String ARGUMENT_EDIT_TASK_ID = "EDIT_TASK_ID";
    private AddEditTaskPresenter mPresenter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.addtask_act);
        // Set up the toolbar.
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();
        checkNotNull(actionBar, "actionBar cannot be null");
        actionBar.setDisplayHomeAsUpEnabled(true);
        actionBar.setDisplayShowHomeEnabled(true);
        final AddEditTaskView addEditTaskView =
                (AddEditTaskView) findViewById(R.id.add_edit_task_view);
        checkNotNull(addEditTaskView, "addEditTaskView not found in layout");
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab_edit_task_done);
        checkNotNull(fab, "fab not found in layout");
        fab.setImageResource(R.drawable.ic_done);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                /*
                 * TODO:
                 * View listeners should simply report the event to the presenter.
                 * In this case: mPresenter.onSavePressed()
                 */
                mPresenter.saveTask(addEditTaskView.getTitle(), addEditTaskView.getDescription());
            }
        });
        String taskId = null;
        if (getIntent().hasExtra(ARGUMENT_EDIT_TASK_ID)) {
            taskId = getIntent().getStringExtra(ARGUMENT_EDIT_TASK_ID);
            actionBar.setTitle(R.string.edit_task);
        } else {
            actionBar.setTitle(R.string.add_task);
        }
        mPresenter = new AddEditTaskPresenter(
                taskId,
                Injection.provideTasksRepository(getApplicationContext()),
                addEditTaskView);
    }
    @Override
    protected void onResume() {
        super.onResume();
        mPresenter.start();
    }
    @Override
    public boolean onSupportNavigateUp() {
        onBackPressed();
        return true;
    }
    @VisibleForTesting
    public IdlingResource getCountingIdlingResource() {
        return EspressoIdlingResource.getIdlingResource();
    }
}

Presenter

  1. Presenter 其內部會持有 View 和 Model 的引用變數。
  2. View 和 Presenter 的交互都會使用其介面的方法來呼叫,而不會呼叫具體的方法。
  3. Presenter的建構式會傳入 View 和 Model 並進行初始化。
  4. Presenter不會依賴於 Android 的 UI 元件,也就是說不會出現 Button, TextView, Toast, Context 等等,只會處理業務邏輯,但可以依賴 RESULT_OK 之纇的整數常數。
  5. Presenter 會有一個 Model 的變數並從該變數來操作資料。
  6. Presenter 通常是處理完邏輯之後,再呼叫 View 的介面更新 UI。

e.g., AddEditTaskPresenter.java

public class AddEditTaskPresenter implements AddEditTaskContract.Presenter,
        TasksDataSource.GetTaskCallback {
    @NonNull
    private final TasksDataSource mTasksRepository;
    @NonNull
    private final AddEditTaskContract.View mAddTaskView;
    @Nullable
    private String mTaskId;
    /**
     * Creates a presenter for the add/edit view.
     *
     * @param taskId ID of the task to edit or null for a new task
     * @param tasksRepository a repository of data for tasks
     * @param addTaskView the add/edit view
     */
    public AddEditTaskPresenter(@Nullable String taskId, @NonNull TasksDataSource tasksRepository,
            @NonNull AddEditTaskContract.View addTaskView) {
        mTaskId = taskId;
        mTasksRepository = checkNotNull(tasksRepository);
        mAddTaskView = checkNotNull(addTaskView);
        mAddTaskView.setPresenter(this);
    }
    @Override
    public void start() {
        if (!isNewTask()) {
            populateTask();
        }
    }
    @Override
    public void saveTask(String title, String description) {
        if (isNewTask()) {
            createTask(title, description);
        } else {
            updateTask(title, description);
        }
    }
    @Override
    public void populateTask() {
        if (isNewTask()) {
            throw new RuntimeException("populateTask() was called but task is new.");
        }
        mTasksRepository.getTask(mTaskId, this);
    }
    @Override
    public void onTaskLoaded(Task task) {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.setTitle(task.getTitle());
            mAddTaskView.setDescription(task.getDescription());
        }
    }
    @Override
    public void onDataNotAvailable() {
        // The view may not be able to handle UI updates anymore
        if (mAddTaskView.isActive()) {
            mAddTaskView.showEmptyTaskError();
        }
    }
    private boolean isNewTask() {
        return mTaskId == null;
    }
    private void createTask(String title, String description) {
        Task newTask = new Task(title, description);
        if (newTask.isEmpty()) {
            mAddTaskView.showEmptyTaskError();
        } else {
            mTasksRepository.saveTask(newTask);
            mAddTaskView.showTasksList();
        }
    }
    private void updateTask(String title, String description) {
        if (isNewTask()) {
            throw new RuntimeException("updateTask() was called but task is new.");
        }
        mTasksRepository.saveTask(new Task(title, description, mTaskId));
        mAddTaskView.showTasksList(); // After an edit, go back to the list.
    }
}

TasksDataSource mTasksRepository 就是 Model。在建構式中傳入 View 和 Model,並呼叫 View 的 setPresenter 綁定 Presenter 自己。

從 updateTask 方法可以看到 先呼叫 Model 改變資料(mTasksRepository.saveTask)
接著再呼叫 View 顯示內容(mAddTaskView.showTasksList)

e.g.,

    private void updateTask(String title, String description) {
        if (isNewTask()) {
            throw new RuntimeException("updateTask() was called but task is new.");
        }
        mTasksRepository.saveTask(new Task(title, description, mTaskId));
        mAddTaskView.showTasksList(); // After an edit, go back to the list.
    }

Model

Model 為 TaskRepository,TaskRepository 繼承 TasksDataSource,而 TaskDataSource 實際上是一個 interface,其中 2 個內部介面 LoadTasksCallback 和 GetTasksCallback 用來作 callback 使用,內部介面的 onTasksLoaded 方法用來當取得 task 成功之後把 task 傳回呼叫點的用途,而 onDataNotAvailable 方法用來當取得 task 失敗後的後續處理。
其餘在 TasksDataSource 介面的方法都是改變資料的共用方法,只要是 Model 都要實作這些方法。
TasksDataSource.java

public interface TasksDataSource {
    interface LoadTasksCallback {
        void onTasksLoaded(List<Task> tasks);
        void onDataNotAvailable();
    }
    interface GetTaskCallback {
        void onTaskLoaded(Task task);
        void onDataNotAvailable();
    }
    void getTasks(@NonNull LoadTasksCallback callback);
    void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback);
    void saveTask(@NonNull Task task);
    void completeTask(@NonNull Task task);
    void completeTask(@NonNull String taskId);
    void activateTask(@NonNull Task task);
    void activateTask(@NonNull String taskId);
    void clearCompletedTasks();
    void refreshTasks();
    void deleteAllTasks();
    void deleteTask(@NonNull String taskId);
}

關於Model的具體介紹參考這篇

總結

  1. View 和 Model 不會有依賴
  2. View 和 Presenter 都是透過介面互相呼叫,而不是透過具體方法
  3. Presenter 不會依賴於 Android 的 UI 元件,也就是說不會出現 Button, Toast, EditText, Context 等等
  4. View 會提供 Presenter 處理完邏輯後顯示內容的方法

接下來使用 MVP 改寫 MVC Pattern in Android 的亂數產生器

Contract

首先建立 MVPContract 介面,該介面中一樣包含 View 和 Presenter 兩個互相對應的內部介面,View 介面一開始可能只有 setPresenter 方法隨後跟著具體子類別的完成再逐漸提取方法到介面內。e.g.,

public interface MVPContract {
  interface View {
    void setPresenter(Presenter presenter);
    void setRollNumber(int randomNumber);
  }
  interface Presenter {
    void rollIt();
  }
}

View

View 的部分直接讓 Activity 去繼承 MVPContract.View,並在內持有 MVPContract.Presenter 的變數並複寫 setPresenter 方法。
關於 MVP View 的重點為

  1. 不可依賴 Model
  2. 和 Presenter 只透過介面的方法交互,不可和實體方法交互

e.g.,

public class MVPActivity extends AppCompatActivity implements OnClickListener, MVPContract.View {
  //Main UI
  private Button mRollIt;
  private TextView mRollNumber;
  private MVPContract.Presenter mPresenter;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.mvp_activity);
    initData();
    initUI();
  }
  private void initData() {
    mPresenter = new MVPPresenter(this, new MVPModel());
  }
  private void initUI() {
    mRollIt = findViewById(R.id.mvp_roll_it);
    mRollIt.setOnClickListener(this);
    mRollNumber = findViewById(R.id.mvp_show_roll_number);
  }
  @Override
  public void onClick(View view) {
    int uiID = view.getId();
    switch (uiID) {
      case R.id.mvp_roll_it:
        mPresenter.rollIt();
        break;
    }
  }
  @Override
  public void setPresenter(MVPContract.Presenter presenter) {
    mPresenter = presenter;
  }
  @Override
  public void setRollNumber(int randomNumber) {
    mRollNumber.setText(""+randomNumber);
  }
}

Presenter

建立 MVPPresenter 繼承 MVPContract.Presenter,在建構式內關連 View 和 Model。
關於 MVP Presenter 的重點為

  1. 和 View 只透過介面方法交互,不可和實體方法交互
  2. 不可依賴 Android 的 UI 元件,如 Button,EditText,Context 等等
public class MVPPresenter implements MVPContract.Presenter {
  private MVPContract.View mView;
  private MVPModel mModel;
  public MVPPresenter(MVPContract.View view, MVPModel model) {
    mView = view;
    mModel = model;
    mView.setPresenter(this);
  }
  @Override
  public void rollIt() {
    mModel.rollOnce();
    mView.setRollNumber(mModel.getRandomNumber());
  }
}

Model

最後是 Model,Model 並沒有變化。

public class MVPModel {
  private int mRandomNumber;
  public void rollOnce() {
    mRandomNumber = new Random().nextInt(100);
  }
  public int getRandomNumber() {
    return mRandomNumber;
  }
}

以上為MVP Pattern in Android的範例。


以下為MVP相關內容

Model in todo-mvp