অ্যান্ড্রয়েড 8.1 এ আপগ্রেড করার পরে স্টার্টফোরগ্রাউন্ড ব্যর্থ


191

আমার ফোনটি 8.1-এ উন্নীত করার পরে বিকাশকারী পূর্বরূপ দেখুন আমার ব্যাকগ্রাউন্ড পরিষেবাটি আর সঠিকভাবে শুরু হয় না।

আমার দীর্ঘ চলমান পরিষেবায় আমি চলমান বিজ্ঞপ্তি শুরু করার জন্য একটি স্টার্টফোরগ্রাউন্ড পদ্ধতি প্রয়োগ করেছি যা তৈরিতে ডাকা হয়।

    @TargetApi(Build.VERSION_CODES.O)
private fun startForeground() {
    // Safe call, handled by compat lib.
    val notificationBuilder = NotificationCompat.Builder(this, DEFAULT_CHANNEL_ID)

    val notification = notificationBuilder.setOngoing(true)
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .build()
    startForeground(101, notification)
}

ভুল বার্তা:

11-28 11:47:53.349 24704-24704/$PACKAGE_NAMEE/AndroidRuntime: FATAL EXCEPTION: main
    Process: $PACKAGE_NAME, PID: 24704
    android.app.RemoteServiceException: Bad notification for startForeground: java.lang.RuntimeException: invalid channel for service notification: Notification(channel=My channel pri=0 contentView=null vibrate=null sound=null defaults=0x0 flags=0x42 color=0x00000000 vis=PRIVATE)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1768)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:164)
        at android.app.ActivityThread.main(ActivityThread.java:6494)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

পরিষেবা বিজ্ঞপ্তির জন্য অবৈধ চ্যানেল , স্পষ্টতই আমার পুরানো চ্যানেল DEFAULT_CHANNEL_ID এপিআই 27 এর জন্য আর উপযুক্ত নয় বলে আমি মনে করি। সঠিক চ্যানেলটি কী হবে? আমি ডকুমেন্টেশন দিয়ে দেখার চেষ্টা করেছি


1
এই উত্তরটি আমার সমাধান ছিল।
অ্যালেক্স জোলিগ

উত্তর:


227

বিভিন্ন সমাধানের জন্য কিছুক্ষণ ঝাঁকুনির পরে আমি জানতে পেরেছিলাম যে একটি অবশ্যই Android 8.1 এবং তার চেয়ে বেশি উপরে একটি বিজ্ঞপ্তি চ্যানেল তৈরি করবে।

private fun startForeground() {
    val channelId =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                createNotificationChannel("my_service", "My Background Service")
            } else {
                // If earlier version channel ID is not used
                // https://developer.android.com/reference/android/support/v4/app/NotificationCompat.Builder.html#NotificationCompat.Builder(android.content.Context)
                ""
            }

    val notificationBuilder = NotificationCompat.Builder(this, channelId )
    val notification = notificationBuilder.setOngoing(true)
            .setSmallIcon(R.mipmap.ic_launcher)
            .setPriority(PRIORITY_MIN)
            .setCategory(Notification.CATEGORY_SERVICE)
            .build()
    startForeground(101, notification)
}

@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(channelId: String, channelName: String): String{
    val chan = NotificationChannel(channelId,
            channelName, NotificationManager.IMPORTANCE_NONE)
    chan.lightColor = Color.BLUE
    chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
    val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    service.createNotificationChannel(chan)
    return channelId
}

আমার বোঝাপড়া থেকে ব্যাকগ্রাউন্ড পরিষেবাদিগুলি এখন সাধারণ বিজ্ঞপ্তি হিসাবে প্রদর্শিত হয় যা ব্যবহারকারী তার পরে বিজ্ঞপ্তি চ্যানেলটি অনির্বাচিত করে প্রদর্শন না করার জন্য নির্বাচন করতে পারে।

আপডেট : এছাড়াও প্রয়োজনীয় অ্যান্ড্রয়েড পি হিসাবে অগ্রভাগের অনুমতি যুক্ত করতে ভুলবেন না:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

জবআইন্টেন্ট সার্ভিসের ক্ষেত্রে আমাদের কি এই পরিবর্তনগুলি করা দরকার? নাকি এটি অভ্যন্তরীণভাবে পরিচালনা করছে?
অমৃত

1
IMPORTANCE_DEFAULTপরিবর্তে না কেন IMPORTANCE_NONE?
ব্যবহারকারী 924

1
@ user924 কোটলিন আসলে সুইফ্টের চেয়ে নতুন ভাষা। কোটলিন জাভা প্রতিস্থাপন করে না, এটি অ্যান্ড্রয়েড বিকাশের জন্য জাভার একমাত্র বিকল্প। যদি আপনি এটি ব্যবহার করে দেখেন তবে দেখতে পাবেন এটি সুইফটের বাক্য গঠনের সাথে আসলে বেশ মিল। আমি ব্যক্তিগতভাবে বিশ্বাস করি এটি জাভার চেয়ে ভাল, যদিও টিওব সূচক যা বলেছে তা সত্ত্বেও (সূচকটি কিছুটা প্রতিক্রিয়াবিহীন পক্ষপাতের অধীন)। এটি জাভাতে থাকা নালপয়েন্টারএক্সসেপশন, ভার্বোসিটি এবং অন্যান্য বেশ কয়েকটি বিষয় সহ অনেকগুলি সমস্যার সমাধান করে। সর্বশেষতম গুগল আই / ও অনুসারে, 95% বিকাশকারী যারা অ্যান্ড্রয়েডের জন্য কোটলিন ব্যবহার করেন তারা এতে খুশি।
উপ 6 সংস্থান


1
@ রাভা ভাল এমনকি অ্যাপটিতে আপনার অগ্রভাগ পরিষেবাটি দিয়ে আপনি কী করছেন তা আমি নিশ্চিত নই কারণ ডকুমেন্টেশনটি মিথ্যা বলে না। এটি স্পষ্টভাবে জানিয়েছে, আপনি ম্যানিফেস্টে অনুমতি ব্যতীত অগ্রভাগের পরিষেবা তৈরি করার চেষ্টা করলে আপনি সুরক্ষা গ্রহণ পাবেন।
CopsOnRoad

135

জাভা সমাধান (অ্যান্ড্রয়েড 9.0, এপিআই 28)

আপনার Serviceক্লাসে এটি যুক্ত করুন:

@Override
public void onCreate(){
    super.onCreate();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
        startMyOwnForeground();
    else
        startForeground(1, new Notification());
}

private void startMyOwnForeground(){
    String NOTIFICATION_CHANNEL_ID = "com.example.simpleapp";
    String channelName = "My Background Service";
    NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_NONE);
    chan.setLightColor(Color.BLUE);
    chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
    NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    assert manager != null;
    manager.createNotificationChannel(chan);

    NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID);
    Notification notification = notificationBuilder.setOngoing(true)
            .setSmallIcon(R.drawable.icon_1)
            .setContentTitle("App is running in background")
            .setPriority(NotificationManager.IMPORTANCE_MIN)
            .setCategory(Notification.CATEGORY_SERVICE)
            .build();
    startForeground(2, notification);
}

আপডেট: অ্যান্ড্রয়েড 9.0 পাই (এপিআই 28)

আপনার AndroidManifest.xmlফাইলে এই অনুমতি যুক্ত করুন:

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

দুটি স্টার্টফোরগ্রাউন্ড () কলগুলিতে কোনও অনন্য আইডি ব্যবহার করার কোনও কারণ আছে কি? এর একই বিজ্ঞপ্তি হওয়ার পরে তারা কি এখানে এক হতে পারে না?
কোডি

@ কপসঅনরোড তাই ও এর জন্য বিজ্ঞপ্তি চ্যানেলের কোনও প্রয়োজন নেই?
শ্রুতি ২

2
@ শ্রুতি আপনার অ্যান্ড্রয়েড 9.0 এর কোড সহ অনুমতি যোগ করতে হবে। দুটোই দরকার।
কক্সঅনরোড

1
@ কপসঅনরোড এই ব্যতিক্রমটি হ'ল মারাত্মক ব্যতিক্রম: android.app.RemoteServiceException: কনটেক্সট.স্টার্টফোরগ্রাউন্ড সার্ভিস () তখন সার্ভিস.স্টার্টফোরগ্রাউন্ড () 'কল করেনি
শ্রুতি

2
পরিষেবাটি চলাকালীন কী বিজ্ঞপ্তিটি প্রদর্শন করা এড়ানো সম্ভব?
মাতদেব

29

প্রথম উত্তরটি কেবলমাত্র সেই লোকদের জন্য যারা কোটলিন জানেন তাদের পক্ষে দুর্দান্ত, যারা এখনও এখানে জাভা ব্যবহার করেন আমি তাদের প্রথম উত্তরটি অনুবাদ করি

 public Notification getNotification() {
        String channel;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
            channel = createChannel();
        else {
            channel = "";
        }
        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, channel).setSmallIcon(android.R.drawable.ic_menu_mylocation).setContentTitle("snap map fake location");
        Notification notification = mBuilder
                .setPriority(PRIORITY_LOW)
                .setCategory(Notification.CATEGORY_SERVICE)
                .build();


        return notification;
    }

    @NonNull
    @TargetApi(26)
    private synchronized String createChannel() {
        NotificationManager mNotificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);

        String name = "snap map fake location ";
        int importance = NotificationManager.IMPORTANCE_LOW;

        NotificationChannel mChannel = new NotificationChannel("snap map channel", name, importance);

        mChannel.enableLights(true);
        mChannel.setLightColor(Color.BLUE);
        if (mNotificationManager != null) {
            mNotificationManager.createNotificationChannel(mChannel);
        } else {
            stopSelf();
        }
        return "snap map channel";
    } 

অ্যান্ড্রয়েডের জন্য, পি এই অনুমতিটি অন্তর্ভুক্ত করতে ভুলবেন না

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

জাভা কোড অনুবাদ করার জন্য ধন্যবাদ। এটি জাভা প্রকল্পগুলির জন্য একটি বড় সহায়তা!
রে লি

17

Andorid 8.1 এ সঠিকভাবে কাজ করে:

নমুনা আপডেট হয়েছে (কোনও অবচয় কোড ছাড়াই):

public NotificationBattery(Context context) {
    this.mCtx = context;

    mBuilder = new NotificationCompat.Builder(context, CHANNEL_ID)
            .setContentTitle(context.getString(R.string.notification_title_battery))
            .setSmallIcon(R.drawable.ic_launcher)
            .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
            .setChannelId(CHANNEL_ID)
            .setOnlyAlertOnce(true)
            .setPriority(NotificationCompat.PRIORITY_MAX)
            .setWhen(System.currentTimeMillis() + 500)
            .setGroup(GROUP)
            .setOngoing(true);

    mRemoteViews = new RemoteViews(context.getPackageName(), R.layout.notification_view_battery);

    initBatteryNotificationIntent();

    mBuilder.setContent(mRemoteViews);

    mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

    if (AesPrefs.getBooleanRes(R.string.SHOW_BATTERY_NOTIFICATION, true)) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID, context.getString(R.string.notification_title_battery),
                    NotificationManager.IMPORTANCE_DEFAULT);
            channel.setShowBadge(false);
            channel.setSound(null, null);
            mNotificationManager.createNotificationChannel(channel);
        }
    } else {
        mNotificationManager.cancel(Const.NOTIFICATION_CLIPBOARD);
    }
}

পুরানো টুকরো টুকরো করা (এটি একটি পৃথক অ্যাপ্লিকেশন - উপরের কোডের সাথে সম্পর্কিত নয় ):

@Override
public int onStartCommand(Intent intent, int flags, final int startId) {
    Log.d(TAG, "onStartCommand");

    String CHANNEL_ONE_ID = "com.kjtech.app.N1";
    String CHANNEL_ONE_NAME = "Channel One";
    NotificationChannel notificationChannel = null;
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
        notificationChannel = new NotificationChannel(CHANNEL_ONE_ID,
                CHANNEL_ONE_NAME, IMPORTANCE_HIGH);
        notificationChannel.enableLights(true);
        notificationChannel.setLightColor(Color.RED);
        notificationChannel.setShowBadge(true);
        notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        manager.createNotificationChannel(notificationChannel);
    }

    Bitmap icon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
    Notification notification = new Notification.Builder(getApplicationContext())
            .setChannelId(CHANNEL_ONE_ID)
            .setContentTitle(getString(R.string.obd_service_notification_title))
            .setContentText(getString(R.string.service_notification_content))
            .setSmallIcon(R.mipmap.ic_launcher)
            .setLargeIcon(icon)
            .build();

    Intent notificationIntent = new Intent(getApplicationContext(), MainActivity.class);
    notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
    notification.contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, notificationIntent, 0);

    startForeground(START_FOREGROUND_ID, notification);

    return START_STICKY;
}

2
উপরের কোডটির কিছু অংশ এখন অবহিত করা হয়েছে, যা আপনি পরিবর্তন Notification.Builder(getApplicationContext()).setChannelId(CHANNEL_ONE_ID)...করে কাটিয়ে উঠতে পারেনNotification.Builder(getApplicationContext(), CHANNEL_ONE_ID)...
নিষিদ্ধ-জিওঞ্জিনিয়ারিং

1
@ নিষিদ্ধ-জিওঞ্জিনিয়ারিং আপনি একেবারে ঠিক বলেছেন ... আমি নতুন নমুনা কোড যুক্ত করেছি। ধন্যবাদ।
মার্টিন ফেফার

কেন PRIORITY_MAXব্যবহার করা ভাল?
ব্যবহারকারী 924

7

আমার ক্ষেত্রে, কারণ কারণটি উল্লেখ না করে আমরা একটি বিজ্ঞপ্তি পোস্ট করার চেষ্টা করেছি NotificationChannel:

public static final String NOTIFICATION_CHANNEL_ID_SERVICE = "com.mypackage.service";
public static final String NOTIFICATION_CHANNEL_ID_TASK = "com.mypackage.download_info";

public void initChannel(){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        nm.createNotificationChannel(new NotificationChannel(NOTIFICATION_CHANNEL_ID_SERVICE, "App Service", NotificationManager.IMPORTANCE_DEFAULT));
        nm.createNotificationChannel(new NotificationChannel(NOTIFICATION_CHANNEL_ID_INFO, "Download Info", NotificationManager.IMPORTANCE_DEFAULT));
    }
}

উপরের কোডটি রাখার সর্বোত্তম জায়গাটি ক্লাসে onCreate()পদ্ধতিতে রয়েছে Application, যাতে আমাদের কেবল একবারে এটি ঘোষণা করা দরকার:

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        initChannel();
    }
}

আমরা এটি সেট আপ করার পরে, আমরা channelIdসুনির্দিষ্টভাবে নির্দিষ্ট করে দিয়ে বিজ্ঞপ্তিটি ব্যবহার করতে পারি:

Intent i = new Intent(this, MainActivity.class);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pi = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID_INFO);
            .setContentIntent(pi)
            .setWhen(System.currentTimeMillis())
            .setContentTitle("VirtualBox.exe")
            .setContentText("Download completed")
            .setSmallIcon(R.mipmap.ic_launcher);

তারপরে, আমরা একটি বিজ্ঞপ্তি পোস্ট করতে এটি ব্যবহার করতে পারি:

int notifId = 45;
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
nm.notify(notifId, builder.build());

আপনি যদি অগ্রভাগের পরিষেবাটির বিজ্ঞপ্তি হিসাবে এটি ব্যবহার করতে চান:

startForeground(notifId, builder.build());

1
অবিচ্ছিন্ন NOTIFICATION_CHANNEL_ID_TASK (২ য় লাইন) কি NOTIFICATION_CHANNEL_ID.gFO হওয়া উচিত?
টিমোরস

টিমোরস, না আপনি এটি আপনার নিজের ধ্রুবক দিয়ে প্রতিস্থাপন করতে পারেন।
অ্যাংগ্রায়ুডি এইচ

4

@ কপসঅনরোডকে ধন্যবাদ, তার সমাধানটি একটি বড় সহায়তা ছিল তবে কেবল এসডিকে ২K এবং উচ্চতরর জন্য কাজ করে। আমার অ্যাপ্লিকেশনটি 24 এবং তারও বেশি লক্ষ্যবস্তু।

অ্যান্ড্রয়েড স্টুডিওতে অভিযোগ করা থেকে বিরত রাখতে আপনার বিজ্ঞপ্তির চারদিকে সরাসরি শর্তসাপেক্ষ দরকার। কোডটি কোনও পদ্ধতিতে VERSION_CODE.O এর শর্তসাপেক্ষে রয়েছে তা জানা যথেষ্ট স্মার্ট নয়।

@Override
public void onCreate(){
    super.onCreate();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
        startMyOwnForeground();
    else
        startForeground(1, new Notification());
}

private void startMyOwnForeground(){

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){

        String NOTIFICATION_CHANNEL_ID = "com.example.simpleapp";
        String channelName = "My Background Service";
        NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_NONE);
        chan.setLightColor(Color.BLUE);
        chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
        NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        assert manager != null;
        manager.createNotificationChannel(chan);

        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID);
        Notification notification = notificationBuilder.setOngoing(true)
                .setSmallIcon(AppSpecific.SMALL_ICON)
                .setContentTitle("App is running in background")
                .setPriority(NotificationManager.IMPORTANCE_MIN)
                .setCategory(Notification.CATEGORY_SERVICE)
                .build();
        startForeground(2, notification);
    }
}

আপনি এই কোডটিতে কি পরিবর্তন করেছেন দয়া করে তা দয়া করে ব্যাখ্যা করতে পারেন, আমি এটি পাইনি।
কপসঅনরোড

সংস্করণ 8.0 এবং অ্যান্ড্রয়েড পাই নিখুঁতভাবে কাজ করে। তবে কেন কেবলমাত্র 8.1 সংস্করণে আমাদের বিজ্ঞপ্তি চ্যানেলের প্রয়োজন?
থমরাই টি

2

এটি আমার পক্ষে কাজ করেছে। আমার পরিষেবা শ্রেণিতে, আমি নীচে Android 8.1 এর জন্য বিজ্ঞপ্তি চ্যানেল তৈরি করেছি:

public class Service extends Service {

    public static final String NOTIFICATION_CHANNEL_ID_SERVICE = "com.package.MyService";
    public static final String NOTIFICATION_CHANNEL_ID_INFO = "com.package.download_info";

    @Override
    public void onCreate() {

        super.onCreate();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            nm.createNotificationChannel(new NotificationChannel(NOTIFICATION_CHANNEL_ID_SERVICE, "App Service", NotificationManager.IMPORTANCE_DEFAULT));
            nm.createNotificationChannel(new NotificationChannel(NOTIFICATION_CHANNEL_ID_INFO, "Download Info", NotificationManager.IMPORTANCE_DEFAULT));
        } else {
            Notification notification = new Notification();
            startForeground(1, notification);
        }
    }
}

দ্রষ্টব্য: আপনি যে চ্যানেলটির জন্য বিজ্ঞপ্তি তৈরি করছেন তা তৈরি করুন Build.VERSION.SDK_INT >= Build.VERSION_CODES.O


-1

এখানে আমার সমাধান

private static final int NOTIFICATION_ID = 200;
private static final String CHANNEL_ID = "myChannel";
private static final String CHANNEL_NAME = "myChannelName";

private void startForeground() {

    final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
            getApplicationContext(), CHANNEL_ID);

    Notification notification;



        notification = mBuilder.setTicker(getString(R.string.app_name)).setWhen(0)
                .setOngoing(true)
                .setContentTitle(getString(R.string.app_name))
                .setContentText("Send SMS gateway is running background")
                .setSmallIcon(R.mipmap.ic_launcher)
                .setShowWhen(true)
                .build();

        NotificationManager notificationManager = (NotificationManager) getApplication().getSystemService(Context.NOTIFICATION_SERVICE);

        //All notifications should go through NotificationChannel on Android 26 & above
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
                    CHANNEL_NAME,
                    NotificationManager.IMPORTANCE_DEFAULT);
            notificationManager.createNotificationChannel(channel);

        }
        notificationManager.notify(NOTIFICATION_ID, notification);

    }

আশা করি এটি সাহায্য করবে :)


1
আপনার সমাধানের যুক্তি ব্যাখ্যা করতে দয়া করে কিছুটা সময় নিন।
স্ট্রিয়া
আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.