반응형

앱을 간단하게 버그를 고치고 다시 올렸는데 이번에는 이상하게 한방에 통과를 안시켜주더라.

 

AppTrackingTransparency 문제 발생

요런 문제가 있다고 한다.

 

바이너리가 거부됨

2020년 11월 7일

If your app integrates AppTrackingTransparency, please indicate where in your app we can find the AppTrackingTransparency permission


Guideline 2.1 - Information Needed



We're looking forward to completing the review of your app, but we need more information to continue. Specifically, we noticed that your app uses the AppTrackingTransparency framework, but we haven't been able to locate the relevant AppTrackingTransparency permission requests.

While it is not required to implement AppTrackingTransparency at this time, we check to make sure the implementation is compliant with our guidelines when we detect the framework in an app.

Next Steps

If your app integrates AppTrackingTransparency, please indicate where in your app we can find the AppTrackingTransparency permission request.

If your app does not integrate AppTrackingTransparency, please indicate this information in the Review Notes section for each version of your app in App Store Connect when submitting for review.

Resources
See the app privacy question update.
Learn more about how AppTrackingTransparency protects user's privacy and data.

 

AppTrackingTransparency Pemission requests 를 넣은 기억이 없어서 다시 검색해봤다. 

 

AppTrackingTransparency Pemission requests란? 

iOS 14 부터는 앱에 포함된 광고 등에서 타겟팅 광고를 위한 디바이스 구분을 위한 식별값 IDFA를 사용해도 되는지 사용자에게 묻는 동의다. 딱 한번 요청을 받을 수 있다. 앱에 구현할 경우 아래와 같이 보인다.

이때 사용자가 직접 수정할 수 있는 문구는 저기 빨간 동그라미 안쪽 문구 뿐이다.

자세한 정보는 Prepare for iOS 14+ 에서 확인 가능하다. 

Flutter 에서는 어떻게 설정하나?

사용하는 패키지에 따라 해당 설정이 필요한 경우가 있는데, 내 경우에는 admob_flutter 플러그인 적용 중 포함되었다.

ios/Runner/info.plist  파일을 열어서 아래와 같이 추가되어있을 경우 해당 프레임워크가 포함된다고 한다.

	<key>NSUserTrackingUsageDescription</key>
	<string>This identifier will be used to deliver personalized ads to you.</string>

 

앱 심사는 어떻게??

일단 적용된 스크린샷과 위치를 알려주는 답장을 쓰고 기다리고 있다.

과연 어떻게 될런지?? 

 

-> 하루 뒤 Pass! 

반응형
반응형

앱을 처음부터 끝까지 모두 만들 수 없다.

기본적인 도구나 구조들은 인터넷을 찾아보면 공개된 내용도 많다.

 

특히 flutter 는 pub.dev에 공개된 플러그인들이 매우 많다. 여기서 잘 검색하고 like 가 많고 score가 높은 패키지를 쓰면 버그가 상대적으로 적고 만족도가 높을 것이다.

pub.dev

 

Dart packages

Pub is the package manager for the Dart programming language, containing reusable libraries & packages for Flutter, AngularDart, and general Dart programs.

pub.dev

 

이번에 우리가 필요로 하는 패키지를 정리하면 아래와 같다.

필요한 패키지(플러그인)들은 pubspec.yaml 파일을 열어서 dependencies 추가해준 후 Pub get 버튼으로 모두 사용 가능하게 해주자. 나중에 플러그인이 너무 많이 업데이트 되서 사용 못할 수 있으니 버전을 같이 써주자.

dependencies:
  flutter:
    sdk: flutter
  share: ^0.6.5+4
  device_info: ^0.4.2+9
  admob_flutter: ^1.0.1
  advertising_id: ^1.0.0 

요렇게 만들어서 사용할 예정이다.

 

 

그리고 device_info 예제를 그대로 긁어다가 main.dart 에 붙여넣고 실행해보자.

첫 실행은 매우 오래걸리니(1~10분) 안된다고 너무 걱정하지 말자.

잠시 다른 생각을 하다보면 뜬다.

// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs

import 'dart:async';

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:device_info/device_info.dart';

void main() {
  runZoned(() {
    runApp(MyApp());
  }, onError: (dynamic error, dynamic stack) {
    print(error);
    print(stack);
  });
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
  Map<String, dynamic> _deviceData = <String, dynamic>{};

  @override
  void initState() {
    super.initState();
    initPlatformState();
  }

  Future<void> initPlatformState() async {
    Map<String, dynamic> deviceData;

    try {
      if (Platform.isAndroid) {
        deviceData = _readAndroidBuildData(await deviceInfoPlugin.androidInfo);
      } else if (Platform.isIOS) {
        deviceData = _readIosDeviceInfo(await deviceInfoPlugin.iosInfo);
      }
    } on PlatformException {
      deviceData = <String, dynamic>{
        'Error:': 'Failed to get platform version.'
      };
    }

    if (!mounted) return;

    setState(() {
      _deviceData = deviceData;
    });
  }

  Map<String, dynamic> _readAndroidBuildData(AndroidDeviceInfo build) {
    return <String, dynamic>{
      'version.securityPatch': build.version.securityPatch,
      'version.sdkInt': build.version.sdkInt,
      'version.release': build.version.release,
      'version.previewSdkInt': build.version.previewSdkInt,
      'version.incremental': build.version.incremental,
      'version.codename': build.version.codename,
      'version.baseOS': build.version.baseOS,
      'board': build.board,
      'bootloader': build.bootloader,
      'brand': build.brand,
      'device': build.device,
      'display': build.display,
      'fingerprint': build.fingerprint,
      'hardware': build.hardware,
      'host': build.host,
      'id': build.id,
      'manufacturer': build.manufacturer,
      'model': build.model,
      'product': build.product,
      'supported32BitAbis': build.supported32BitAbis,
      'supported64BitAbis': build.supported64BitAbis,
      'supportedAbis': build.supportedAbis,
      'tags': build.tags,
      'type': build.type,
      'isPhysicalDevice': build.isPhysicalDevice,
      'androidId': build.androidId,
      'systemFeatures': build.systemFeatures,
    };
  }

  Map<String, dynamic> _readIosDeviceInfo(IosDeviceInfo data) {
    return <String, dynamic>{
      'name': data.name,
      'systemName': data.systemName,
      'systemVersion': data.systemVersion,
      'model': data.model,
      'localizedModel': data.localizedModel,
      'identifierForVendor': data.identifierForVendor,
      'isPhysicalDevice': data.isPhysicalDevice,
      'utsname.sysname:': data.utsname.sysname,
      'utsname.nodename:': data.utsname.nodename,
      'utsname.release:': data.utsname.release,
      'utsname.version:': data.utsname.version,
      'utsname.machine:': data.utsname.machine,
    };
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text(
              Platform.isAndroid ? 'Android Device Info' : 'iOS Device Info'),
        ),
        body: ListView(
          children: _deviceData.keys.map((String property) {
            return Row(
              children: <Widget>[
                Container(
                  padding: const EdgeInsets.all(10.0),
                  child: Text(
                    property,
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                Expanded(
                    child: Container(
                  padding: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0),
                  child: Text(
                    '${_deviceData[property]}',
                    maxLines: 10,
                    overflow: TextOverflow.ellipsis,
                  ),
                )),
              ],
            );
          }).toList(),
        ),
      ),
    );
  }
}

 

이렇게 기본적인 화면이 뜬다.

이 화면을 예쁘게 만들고 admob 을 추가해주고 올리면 되겠지?

 

이번엔 여기까지,,, 

 

 

반응형

'Flutter 프로그래밍' 카테고리의 다른 글

안드로이드 앱 만들기(1) - 준비 과정  (0) 2020.10.31
반응형

간단한 Device ID를 보여주는 앱을 만들어보자.

앱을 만들고 Play store colsol에 등록하는 과정까지 진행해보자.

 

 

준비

먼저 안드로이드 스튜디오를 설치한다.

developer.android.com/studio/install?hl=ko

 

Android 스튜디오 설치  |  Android 개발자  |  Android Developers

Windows, macOS 또는 Linux에서 Android 스튜디오를 설정 및 설치합니다.

developer.android.com

flutter SDK 도 설치해준다. 폴더는 알아서..

flutter-ko.dev/docs/get-started/install

 

설치

Flutter를 설치할 운영 체제를 선택해주세요:{{site.alert.note}} **Are you on Chrome OS?** If so, see the official [Chrome OS Flutter installation docs!](/docs/get-started/install/chromeos){{site.alert.end}}

flutter-ko.dev

설치 후 환경변수 설정도 해준다. 시작메뉴에서 '환경 변수' 로 검색해서 진입 후 환경변수 설정도 해준다.

그리고 안드로이드 스튜디오에서 setting -> Plugins -> Marketplace 에서 flutter 플러그인을 설치한다. dart 도 같이 설치된다.

셋팅에서 SDK 를 검색해서 필요한 sdk를 다운받고

sdk-tool도 필요한것 받자.

android SDK build-tool 과 Android Emulator와 Android SDK Platform-Tools 는 꼭 필요하다.

기타 필요한 것들도 받으면 된다.

 

 

안드로이드 폰과 adb 연결을 위해 케이블로 연결하거나 에뮬레이터에 적당한 녀석을 받아서 실행해놓자.

오른쪽 위에 AVD Manager에 들어가서 Create Virtual.... 어쩌고를 누르고 phone 에서 적당한 녀석으로 받자.

픽셀로 가면 최신폰, 넥서스로 가면 구형폰이다.

 

 

 

 

이제 File -> New -> New Flutter Project 를 누르고

(프로젝트 시작 전이면 File 선택을 안할수도 있다... 셋팅에 따라 다름)

Flutter Application 을 누른다.

아래처럼 대충 프로젝트 이름을 넣어준다. 

 

여기서 pakage name 을 넣어줘야 하는데 이건 플레이 스토어에 올릴 때 유일한 이름이 되어야 한다.

보통 웹사이트 주소 역순으로 넣는식으로 많이 한다.

qsurf.tistroy.com  이 내가 가진 도메인이면 com.tistory.qsurf.device_info_app 이런식으로 만들면 겹칠 확률이 매우 낮다.

 

 

 

그럼 이제 간단한 예제가 들어있는 플러터 앱이 만들어졌다.

shift+F10 을 누르거나 메뉴에서 잘 찾아서 run을 누르면 아래와 같이 + 를 누르면 숫자가 올라가는 앱이 실행된다.

여기서 아이폰용 애뮬레이터는 맥에서만 사용 가능하다.

 

오늘은 여기까지

반응형
반응형

구글 admob 을 적용시켜서 테스트를 하다 보면

Failed to load ad: 0 라는 메세지를 받게 된다.

 

원인은 안드로이드/iOS 폰에서 직접 테스트 할 때 정식으로 배포된 앱이 아닌 경우 광고를 불러오지 못해서다.

이때 test로 사용할 장치 device id 를 admob 에 미리 넣어놓으면 해당 id를 가진 장치에서는 test ad가 뜬다.

 

 

Java / Kotlin

device id 를 얻는 방법은 java 와 kotlin 관련 문서가 많다.

 

해당 장치에서 앱을 실행하고 logCat 에서 adRequest 를 검색해보면 아래처럼 나온다고 한다. 여기서 보이는 Device ID를 복사해서 쓰면 된다.

 

 12-29 11:18:25.615: I/Ads(2132): To get test ads on this device, call adRequest.addTestDevice("D9XXXXXXXXXXXXXXXXXXXXXXXXXXXXX");".

 

 

 

Flutter

 

Flutter / 안드로이드 스튜디오 사용중인데 logCat 에서 잘 안보여서 다른 방법을 찾아봤다.

device_id 를 얻어오는 많은 plugin 이 있는데 그중 하나를 골라서 사용하면 된다.

 

아래 plugin을 사용해봤다.

 

 

 

플러그인 설치

패키지를 pubspec.yaml 파일에 넣어주고 pubget 을 해준다.

dependencies:
  device_id: ^0.2.0

 

사용을 원하는 파일에 import 

import 'package:device_id/device_id.dart';

 

예제 코드

아래와 같이 DeviceId.getID 로 deviceID를 얻어와서 사용하면 된다.

import 'package:flutter/material.dart';
import 'package:device_id/device_id.dart';

void main() => runApp(new MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => new _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String deviceID;

  void getDeviceID() async {
    String deviceId = await DeviceId.getID;
    deviceID = 'Device ID is $deviceId';
    print('Device ID is $deviceId');
  }

  @override
  Widget build(BuildContext context) {
    getDeviceID();
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Text('$deviceID'),
        ),
      ),
    );
  }
}

 

반응형
반응형

안드로이드 / iOS 앱을 만들다보면 여러가지 권한을 사용해야 할 때가 있다.

카메라, 저장장치, 위치정보, 메세지, 연락처 등등 여러 정보를 기기에서 받아서 활용하는 경우가 많다.

 

만약 적절한 권한이 없는 경우 디버그 모드에서는 동작하는데 컴파일 후 장치에서 실행하면 오류와 함께 종료되는 경우가 많다.

 

다행히도 flutter 에 적절한 패키지가 있다. 패키지가 있을 경우에는 정말 사용하기 편하다.

 

Permission Handler 5.0.1+1

현시점에서 버전은 5.0.1+1 이다. 

pub.dev/packages/permission_handler

 

permission_handler | Flutter Package

Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions.

pub.dev

 

사용전 설정

안드로이드 

  • pre 1.12 android project 로 업그레이드 후 사용
  • gladdle.properties 파일에 아래와 같이 추가
android.useAndroidX=true
android.enableJetifier=true
  • 'android/app/build.gradle' 파일에서 android sdk 버전 29로 설정
android {
  compileSdkVersion 28
  ...
}

 

iOS

Info.plist 에 권한 추가

아래 링크에서 모든 종류의 권한 예시 확인 가능

github.com/Baseflow/flutter-permission-handler/blob/develop/permission_handler/example/ios/Runner/Info.plist

 

 

사용법

pubspec.yaml 파일 depency 에 추가해준다.

  permission_handler: ^5.0.1+1

사용하려는 dart 파일에 import 추가 후 사용

import 'package:permission_handler/permission_handler.dart';

 

권한 상태 확인(및 종류)

권한 상태는 undetermined, granted, denied, restricted 가 있으며 안드로이드는 추가로 permanentlyDenied 가 있다.

예를 들어 카메라 권한 상태가 궁금하면 Permission.camera.status 로 확인하면 된다.

var status = await Permission.camera.status;
if (status.isUndetermined) {
  // We didn't ask for permission yet.
}

// You can can also directly ask the permission about its status.
if (await Permission.location.isRestricted) {
  // The OS restricts access, for example because of parental controls.
}

 

권한 요청

아래와 같이 Permission.contacts.request() 로 요청하면 된다. 

return 값은 undetermined, granted, denied, restricted 와 같은 값이다.

if (await Permission.contacts.request().isGranted) {
  // Either the permission was already granted before or the user just granted it.
}

// You can request multiple permissions at once.
Map<Permission, PermissionStatus> statuses = await [
  Permission.location,
  Permission.storage,
].request();
print(statuses[Permission.location]);

 

 

사용예시

실제로 사용할 때는 await 가 있기 때문에 async 로 감싸진 클래스(또는 함수) 에서 다뤄야 한다.

위젯 생명주기중 하나인 initState 에서는 Future 클래스가 호출이 안된다. 

이럴때는 WidgetsBinding.instance.addPostFrameCallback((_) { 요기 }  에다가 넣어주면 된다.

 

아래와 같은 함수를 만들어놓고 불러오면 된다.

여기서 카메라 권한 확인용으로 만든 camPermissionIsGranted 변수를 사용했다.

처음엔 false 로 사용 불가능으로 해놓고 권한이 확인되면 true 로 변경하자.

 

bool camPermissionsGranted = false;

Future<bool> CamPermissionIsGranted() async {
  var _status = await Permission.camera.status.isGranted;
  camPermissionsGranted = _status;
  return _status;
}

 

 

권한이 있을때와 없을때 widget 을 어떻게 구현하는지가 처음에 햇갈렸다.

FutureBuilder로 구현해봤는데 뭔가 배보다 배꼽이 큰거같아서 다시 검색해보니 더 쉽고 깔끔한 방법이 있다.

 

예를들어 Scaffold의 body 에 구현한다고 하면 아래 ? : 연산자를 쓰면 쉽다.

그리고 권한 확인이 되면 camPermissionIsGranted 의 값을 바꾸고 동시에 setState로 위젯을 다시 build 하면 된다.

(조건) ? (true인 경우 실행) : (false인 경우 실행) 

return Scaffold(
  body: camPermissionsGranted 
          ? Container(child: 퍼미션 있을경우 실행)
          : Container(child: 퍼미션 없을 경우 실행)

 

아래 예제 맨 아랫줄에서 보면 setState까지 구현된 권한 요청 함수를 볼 수 있다.

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';

void main() => runApp(MyApp());

/// Example Flutter Application demonstrating the functionality of the
/// Permission Handler plugin.
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
          actions: <Widget>[
            IconButton(
              icon: const Icon(Icons.settings),
              onPressed: () async {
                var hasOpened = openAppSettings();
                debugPrint('App Settings opened: ' + hasOpened.toString());
              },
            )
          ],
        ),
        body: Center(
          child: ListView(
              children: Permission.values
                  .where((Permission permission) {
                    if (Platform.isIOS) {
                      return permission != Permission.unknown &&
                          permission != Permission.sms &&
                          //permission != Permission.storage &&
                          permission != Permission.ignoreBatteryOptimizations &&
                          permission != Permission.accessMediaLocation;
                    } else {
                      return permission != Permission.unknown &&
                          permission != Permission.mediaLibrary &&
                          permission != Permission.photos &&
                          permission != Permission.reminders;
                    }
                  })
                  .map((permission) => PermissionWidget(permission))
                  .toList()),
        ),
      ),
    );
  }
}

/// Permission widget which displays a permission and allows users to request
/// the permissions.
class PermissionWidget extends StatefulWidget {
  /// Constructs a [PermissionWidget] for the supplied [Permission].
  const PermissionWidget(this._permission);

  final Permission _permission;

  @override
  _PermissionState createState() => _PermissionState(_permission);
}

class _PermissionState extends State<PermissionWidget> {
  _PermissionState(this._permission);

  final Permission _permission;
  PermissionStatus _permissionStatus = PermissionStatus.undetermined;

  @override
  void initState() {
    super.initState();

    _listenForPermissionStatus();
  }

  void _listenForPermissionStatus() async {
    final status = await _permission.status;
    setState(() => _permissionStatus = status);
  }

  Color getPermissionColor() {
    switch (_permissionStatus) {
      case PermissionStatus.denied:
        return Colors.red;
      case PermissionStatus.granted:
        return Colors.green;
      default:
        return Colors.grey;
    }
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(_permission.toString()),
      subtitle: Text(
        _permissionStatus.toString(),
        style: TextStyle(color: getPermissionColor()),
      ),
      trailing: IconButton(
          icon: const Icon(Icons.info),
          onPressed: () {
            checkServiceStatus(context, _permission);
          }),
      onTap: () {
        requestPermission(_permission);
      },
    );
  }

  void checkServiceStatus(BuildContext context, Permission permission) async {
    Scaffold.of(context).showSnackBar(SnackBar(
      content: Text((await permission.status).toString()),
    ));
  }

  Future<void> requestPermission(Permission permission) async {
    final status = await permission.request();

    setState(() {
      print(status);
      _permissionStatus = status;
      print(_permissionStatus);
    });
  }
}

 

 

추가로 안드로이드의 경우 권한 수락을 완전히 거부할 경우 있는데 그럴때는 앱정보에 들어가서 직접 권한을 줘야 한다.

아래 루틴을 requestPermission 에 넣어주면 좋다.

if (await Permission.speech.isPermanentlyDenied) {
  // 유저가 완전히 권한을 거부하였을 경우 앱설정으로 진입
  // 유저가 알아보기 쉽게 도움말을 띄우면 더 좋음
  openAppSettings();
}

 

이상 끝.

반응형

+ Recent posts