Application의 설치 또는 삭제 이벤트 획득하기

|

안드로이드에서 어플리케이션(이하 App)을 설치하거나 삭제할 때는 그 이벤트가 Broadcast로 전달됩니다. 즉, BroadcastReceiver를 등록해놓은 각 App들의 설치/삭제 이벤트를 수신할 수 있습니다.


AndroidManifest.xml

manifest.xml에 다음과 같이 BroadcastReceiver를 추가해줍니다.

<receiver android:name=".PackageEventReceiver">
    <intent-filter>
        <action android:name="android.intent.action.PACKAGE_ADDED"/>
        <action android:name="android.intent.action.PACKAGE_REMOVED"/>
        <action android:name="android.intent.action.PACKAGE_REPLACED"/>
        <data android:scheme="package"/>
    </intent-filter>
</receiver>


PackageEventReceiver.java

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

import android.util.Log;

public class PackageEventReceiver extends BroadcastReceiver {

  @Override
  public void onReceive(Context context, Intent intent) {
    String packageName = intent.getData().getSchemeSpecificPart();
    String action = intent.getAction();

    if(action.equals(Intent.ACTION_PACKAGE_ADDED)) {
      Log.d("", "[snowdeer] Package ADDED : " + packageName);
    } else if(action.equals(Intent.ACTION_PACKAGE_REMOVED)) {
      Log.d("", "[snowdeer] Package REMOVED : " + packageName);
    }
  }
}


Event Receiver 등록

private PackageEventReceiver mPackageEventReceiver = new PackageEventReceiver();

private void registerPackageEventReceiver() {
  registerReceiver(mPackageEventReceiver, new IntentFilter(Intent.ACTION_PACKAGE_ADDED));
}


또는 다음과 같은 코드를 이용해서 등록하면 됩니다.

private PackageEventReceiver mPackageEventReceiver = new PackageEventReceiver();

private void registerPackageEventReceiver() {
  IntentFilter intentFilter = new IntentFilter();
  intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
  intentFilter.addAction(Intent.ACTION_PACKAGE_INSTALL);
  intentFilter.addDataScheme("package");
  registerReceiver(mPackageEventReceiver, intentFilter);
}

App Intro(가이드 도움말) 화면 꾸미기

|

일반적인 AppIntro 화면은 대략 다음과 같은 형태를 하고 있습니다.

image

많은 사람들이 사용하고 있는 오픈소스가 있으며, 여기에서 확인할 수 있습니다.

Dependency 설정

Android Studio에서는 간단히 gradle에 다음 라인만 추가하면 AppIntro 컴포넌트를 사용할 수 있습니다.

dependencies {
    compile 'com.github.paolorotolo:appintro:4.1.0'
}


사용하는 코드는 다음과 같습니다. AppIntro 클래스를 상속받은 Activity를 구현하면 됩니다. 그리고 중요한 점은 onCreate() 함수 내에서 setContentView() 함수는 지워야 한다는 점입니다. 각각의 Intro 화면들은 Fragment를 상속받아서 구현할 수 있습니다.

SplashActivity.java

import android.content.Intent;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;

import com.github.paolorotolo.appintro.AppIntro;

public class SplashActivity extends AppIntro {

  Fragment mSplash1 = new SplashFragment1();
  Fragment mSplash2 = new SplashFragment2();
  Fragment mSplash3 = new SplashFragment3();

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

    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
      Window w = getWindow(); // in Activity's onCreate() for instance
      w.setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
          WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
    }

    addSlide(mSplash1);
    addSlide(mSplash2);
    addSlide(mSplash3);
  }

  @Override
  public void onSkipPressed(Fragment currentFragment) {
    super.onSkipPressed(currentFragment);
    startMainActivity();
  }

  @Override
  public void onDonePressed(Fragment currentFragment) {
    super.onDonePressed(currentFragment);
    startMainActivity();
  }

  @Override
  public void onSlideChanged(@Nullable Fragment oldFragment,
      @Nullable Fragment newFragment) {
    super.onSlideChanged(oldFragment, newFragment);
  }

  @Override
  protected void onResume() {
    super.onResume();
  }

  private void startMainActivity() {
    Intent intent = new Intent(SplashActivity.this, MainActivity.class);
    startActivity(intent);
    finish();
  }
}

Ken Burns Effect View

|

Ken Burns Effect는 패닝(Panning)과 줌(Zooming) 기반으로 사진을 화려하게 보여줄 수 있는 효과(Effect)로 사진으로부터 동영상 등을 뽑아낼 때 많이 사용하고 있습니다.

Ken Burns Effect의 모습은 다음과 같습니다.

KenBurns

이미 아주 많은 사람들이 사용하고 있는 오픈소스가 있으니 그걸 활용하도록 하겠습니다. 여기에서 소스 코드를 확인할 수 있으며, Android Studio에서는 간단히 gradle에 다음 라인만 추가하면 KenBurnsView를 사용할 수 있습니다.


Dependency 설정

dependencies {
    compile 'com.flaviofaria:kenburnsview:1.0.7'
}


Layout 코드

실제로 사용할 때는 다음과 같이 XML에 KenburnsView를 배치하기만 하면 됩니다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/activity_main"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <com.flaviofaria.kenburnsview.KenBurnsView
    android:id="@+id/image"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/image" />

</RelativeLayout>


특별히 Java 코드를 따로 추가하지 않더라도 훌륭한 Ken Burns 효과를 보여주며, Java 코드를 통해 좀 더 다양하고 정교한 동작을 설정할 수 있습니다.

Apache's Http Library 충돌 해결 방법

|

Apache’s Http Library와 최신 버전의 Android SDK와 충돌이 발생하는 현상이 있습니다. 이럴 경우 다음과 같은 에러가 발생할 것입니다.

Warning:WARNING: Dependency org.apache.httpcomponents:httpclient:4.3.5 is ignored for debug as it may be conflicting with the internal version provided by Android.

그리고 빌드는 경고만 뜨고 잘 되더라도, 실행을 하려고 하면 에러가 뜨면서 실행이 안되는 현상이 생기기도 합니다. 이런 경우에는 app의 build.gradle 파일에 다음과 같은 코드를 삽입해주면 해결이 됩니다.


build.gradle

...
android {
   ...
    packagingOptions {
         exclude 'META-INF/NOTICE'
         exclude 'META-INF/LICENSE'
   }
}


전체 코드는 다음과 같습니다.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.0"
    defaultConfig {
        applicationId "com.datacafe.nestcamapi"
        minSdkVersion 16
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    packagingOptions {
        exclude 'META-INF/NOTICE'
        exclude 'META-INF/LICENSE'
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.0.1'
    testCompile 'junit:junit:4.12'
}

Google Awareness API 예제

|

Google Awareness API 사용 예제입니다. 일단 앞서 포스팅한 Google API Key 등록을 먼저 해야 합니다. Google API Key를 획득했다면 다음과 같이 AndroidManifest.xml 에 해당 키 정보를 기입해줍니다.


AndroidManifest.xml

<application
  android:allowBackup="true"
  android:icon="@mipmap/ic_launcher"
  android:label="@string/app_name"
  android:supportsRtl="true"
  android:theme="@style/AppTheme">

  <meta-data
    android:name="com.google.android.awareness.API_KEY"
    android:value="API_KEY" />

  <activity android:name=".MainActivity">
    <intent-filter>
      <action android:name="android.intent.action.MAIN" />

      <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
  </activity>
</application>


Activity_main.xml

그리고 간단한 버튼 4개를 배치해서 테스트를 하기 위한 activity_main.xml 코드는 다음과 같습니다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/activity_main"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:paddingTop="@dimen/activity_vertical_margin"
  android:paddingBottom="@dimen/activity_vertical_margin"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  android:orientation="vertical">

  <Button
    android:id="@+id/btn_get_location"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Get Location" />

  <Button
    android:id="@+id/btn_get_activity"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Get Activity" />

  <Button
    android:id="@+id/btn_get_place"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Get Place" />

  <Button
    android:id="@+id/btn_get_weather"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Get Weather" />

  <ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
      android:id="@+id/textview"
      android:layout_width="match_parent"
      android:layout_height="match_parent" />

  </ScrollView>
</LinearLayout>


MainActivity.java

마지막으로, MainActivity.java 코드입니다.

package com.datacafe.googleawarenessapi;

import android.content.pm.PackageManager;
import android.location.Location;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

import com.google.android.gms.awareness.Awareness;
import com.google.android.gms.awareness.snapshot.DetectedActivityResult;
import com.google.android.gms.awareness.snapshot.LocationResult;
import com.google.android.gms.awareness.snapshot.PlacesResult;
import com.google.android.gms.awareness.snapshot.WeatherResult;
import com.google.android.gms.awareness.state.Weather;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.location.ActivityRecognitionResult;
import com.google.android.gms.location.DetectedActivity;
import com.google.android.gms.location.places.PlaceLikelihood;

import java.util.List;

public class MainActivity extends AppCompatActivity {

  //Activities
  //public static final int IN_VEHICLE = 0;
  //public static final int ON_BICYCLE = 1;
  //public static final int ON_FOOT = 2;
  //public static final int STILL = 3;
  //public static final int UNKNOWN = 4;
  //public static final int TILTING = 5;
  //public static final int WALKING = 7;
  //public static final int RUNNING = 8;

  private GoogleApiClient mGoogleApiClient;
  private TextView mDebugLogView;

  private StringBuilder mLogBuilder = new StringBuilder();

  private void log(String message) {
    mLogBuilder.append(message);
    mLogBuilder.append("\n");

    if(mDebugLogView != null) {
      mDebugLogView.setText(mLogBuilder.toString());
    }
  }

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

    mDebugLogView = (TextView) findViewById(R.id.textview);

    findViewById(R.id.btn_get_location).setOnClickListener(mOnClickListener);
    findViewById(R.id.btn_get_activity).setOnClickListener(mOnClickListener);
    findViewById(R.id.btn_get_place).setOnClickListener(mOnClickListener);
    findViewById(R.id.btn_get_weather).setOnClickListener(mOnClickListener);

    Log.i("snowdeer", "[snowdeer] start...");

    mGoogleApiClient = new GoogleApiClient.Builder(this)
        .addApi(Awareness.API)
        .build();
    mGoogleApiClient.connect();

    Log.i("snowdeer", "[snowdeer] GoogleApi connect..");


  }

  private View.OnClickListener mOnClickListener = new View.OnClickListener() {

    @Override
    public void onClick(View view) {
      switch(view.getId()) {
        case R.id.btn_get_location: {
          if(ContextCompat.checkSelfPermission(
              MainActivity.this,
              android.Manifest.permission.ACCESS_FINE_LOCATION) !=
              PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(
                MainActivity.this,
                new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION},
                100
            );
            return;
          }

          Awareness.SnapshotApi.getLocation(mGoogleApiClient)
              .setResultCallback(new ResultCallback<LocationResult>() {
                @Override
                public void onResult(@NonNull LocationResult locationResult) {
                  if(!locationResult.getStatus().isSuccess()) {
                    log("Could not get location.");
                    return;
                  }
                  Location location = locationResult.getLocation();
                  log("Lat: " + location.getLatitude()
                      + ", Lon: " + location.getLongitude());
                }
              });
          break;
        }
        case R.id.btn_get_activity:
          Awareness.SnapshotApi.getDetectedActivity(mGoogleApiClient)
              .setResultCallback(new ResultCallback<DetectedActivityResult>() {
                @Override
                public void onResult(@NonNull DetectedActivityResult detectedActivityResult) {
                  if(!detectedActivityResult.getStatus().isSuccess()) {
                    log("Could not get the current activity.");
                    return;
                  }
                  ActivityRecognitionResult ar = detectedActivityResult
                      .getActivityRecognitionResult();
                  DetectedActivity probableActivity = ar.getMostProbableActivity();
                  log(probableActivity.toString());
                }
              });
          break;

        case R.id.btn_get_place: {
          if(ContextCompat.checkSelfPermission(
              MainActivity.this,
              android.Manifest.permission.ACCESS_FINE_LOCATION) !=
              PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(
                MainActivity.this,
                new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION},
                100
            );
            return;
          }

          Awareness.SnapshotApi.getPlaces(mGoogleApiClient)
              .setResultCallback(new ResultCallback<PlacesResult>() {
                @Override
                public void onResult(@NonNull PlacesResult placesResult) {
                  if(!placesResult.getStatus().isSuccess()) {
                    log("Could not get places.");
                    return;
                  }
                  List<PlaceLikelihood> placeLikelihoodList = placesResult.getPlaceLikelihoods();

                  if(placeLikelihoodList == null) {
                    log("Result List is Null!!");
                    return;
                  }
                  // Show the top 5 possible location results.
                  for(int i = 0; i < placeLikelihoodList.size(); i++) {
                    PlaceLikelihood p = placeLikelihoodList.get(i);
                    log(p.getPlace().getName().toString()
                        + ", likelihood: " + p.getLikelihood());
                  }
                }
              });
          break;
        }

        case R.id.btn_get_weather: {
          if(ContextCompat.checkSelfPermission(
              MainActivity.this,
              android.Manifest.permission.ACCESS_FINE_LOCATION) !=
              PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(
                MainActivity.this,
                new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION},
                100
            );
            return;
          }

          Awareness.SnapshotApi.getWeather(mGoogleApiClient)
              .setResultCallback(new ResultCallback<WeatherResult>() {
                @Override
                public void onResult(@NonNull WeatherResult weatherResult) {
                  if(!weatherResult.getStatus().isSuccess()) {
                    log("Could not get weather.");
                    return;
                  }
                  Weather weather = weatherResult.getWeather();
                  log("Weather: " + weather);
                }
              });

          break;
        }
      }
    }
  };
}